- 排序算法有很多:冒泡排序/选择排序/插入排序/归并排序/计数排序(counting sort)/基数排序(radix sort)/希尔排序/堆排序/桶排序
封装属性:
//属性
this.array = [];
//方法
//向数据插入到数组中的方法
ArrayList.prototype.insert = function(item){
this.array.push(item);
}
//toString方法
ArrayList.prototype.toString = function(){
//join()把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。
return this.array.join('-');
}
//交换两个数据
ArrayList.prototype.swap = function(x,y){
var temp = this.array[x];
this.array[x] = this.array[y];
this.array[y] = temp;
}
冒泡排序的思路
- 冒泡排序算法相对其他排序运行效率较低但是在概念上它是排序算法中最简单的
思路
- 对未排序的各元素从头到尾依次比较相邻的两个元素大小关系
- 如果左边的队员高,则两队员交换位置
- 向右移动个位置,比较下面两个队员
- 当走到最右端时,最高的队员一定被放在了最右边
- 按照这个思路,从最左端重新开始,这次走到倒数第二个位置的队员即可
源码:
//冒泡排序的封装
ArrayList.prototype.bubbleSort = function(){
//1. 获取数组的长度
var length = this.array.length;
for (var j = length-1; j >= 0; j--) {
for (var i = 0; i < j; i++) {
if (this.array[i] > this.array[i+1]) {
this.swap(i,i+1);
}
}
}
}
冒泡排序的效率
冒泡排序的比较次数:
- 如果按照上面的例子来说一共有7个数字,那么每次循环时进行了几次的比较呢?
- 第一次循环6次比较第二次5次比较第三次4次比..直到最后一趟进行了一 次比较口对于7个数据项比较次数:6+5+4+3+2+1
- 对于N个数据项呢? (N-1)+(N-2)+(N-3)+...+ 1 =N * (N-1)/
- 通过大0表示法推到过程我们来推到下冒泡排序的大O形式:
- N * (N- 1)/2 = N²/2-N/2,根据规则2,只保留最高阶项编程N²/2
N²/ 2,根据规则3,去除常量编程N²
- 因此冒泡排序的大0表示法为O(N²)
冒泡排序的交换次数:
- 如果有两次比较才需要交换一次(不可能每次比较都交换次), 那么交换次数为N²/4
- 由于常量不算在大0表示法中,因此,我们可以认为交换次数的大0表示也是O(N²)
选择排序
- 选择排序改进了冒泡排序
- 将交换的次数由O(N²)减少到O(N)
- 但是比较的次数依然是O(N²)
- 选择排序的思路:
- 选定第一个索引位置,然后和后面元素依次比较
- 如果后面的队员,小于第个索引位置的队员, 则交换位置
- 经过几轮的比较后,可以确定第一个位置是最小的
- 然后使用同样的方法把剩下的元素逐个比较即可
- 可以看出选择排序,第轮会选出最小值,第二轮会选出第二小的值,直到最后
源码:
//选择排序
ArrayList.prototype.selectionSort = function(){
//1. 获取数组的长度
var length = this.array.length;
for (var j = 0; j < length -1 ; j++){
var min = j;
for (var i = min +1;i < length; i++) {
if (this.array[min] > this.array[i]) {
min = i;
}
}
this.swap(min,j);
}
}
选择排序的效率
选择排序的比较次数:
- 选择排序和冒泡排序的比较次数:N * (N-1)/2
- 大O表示法:O(N²)
选择排序的交换次数:
- 选择排序毎次进行选择的吋候最多需要交换1次.一共遍历N - 1次
- 选择排序的交換次数只有N-1次,用大0表示法就是O(N)
- 所以选择排序通常认为在执行效率上是高于冒泡排序的
插入排序
- 局部有序:
- 插入排序思想的核心是局部有序
- 比如在一个队列中的人,我们选择其中一个作为标记的队员
- 这个被标记的队员左边的所有队员已经是局部有序的.
- 这意味着,有一部门人是按顺序排列好的有一 部分还没有顺序
- 插入排序的思路:
- 从第一个元素开始 ,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复上一个步骤,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后,重复上面的步骤.
源码:
//插入排序
ArrayList.prototype.insertionSort = function(){
//1. 获取数组长度
var length = this.array.length;
for (var i = 1; i< length; i++ ) {
var j = i;
var temp = this.array[i];
while (this.array[j-1] > temp && j > 0){
this.array[j] = this.array[j-1];
j--;
}
//2. 将j位置的数据,放置到temp
this.array[j] = temp;
}
}
插入排序的效率
- 插入排序的比较次数:
- 第一趟时, 需要的最多次数是1,第二趟最多次数是2,依次类推,最后趟是N-1次.
- 因此是插入排序的最多次数:1+2+3+...+N-1=N * (N-1)/2
- 然而每趟发现插入点之前,平均只有全体数据项的一 半需要进行比较
- 我们可以除以2得到N * (N-1)/4.所以相对于选择排序,其他比较次数是少了一半的
插入排序的复制次数:
- 第一趟时,需要的最多复制次数是1,第趟最多次数是2.依次类推最后一趟是N-1次
- 因此复制次数最多是1+2+3+...+N-1=N*(N-1)/2
- 平均次数N*(N-1)/4
- 对于基本有序的情况:
- 对于已经有序或基本有序的数据来说,插入排序要好很多
- 当数据有序的时候 while循环的条件总是为假所以它变成了外层循环中的一个简单语句,执行N-1次
- 在这种情况下,算法运行至需要N(N)的时间效率相对来说会更高
- 比较次数是选择排序的半所以这个算法的效率是高于选择排序的
希尔排序
- 希尔原稿的做法
- 选择合适的增量:
- 在希尔排序的原稿中,他建议的初始间距是N/2,简单的把每趟排序分成两半.
- 也就是说,对于N = 100的数组,增量间隔序列为: 50, 25, 12, 6, 3, 1.
- 这个方法的好处是不需要在开始排序前为找合适的增量而进行任何的计算
Hibbard增量序列:
- 增量的算法为2k-1.也就是为1,3,5,7...
- 这种增量的最坏复杂度为O(N3/2),猜想的平均复杂度为O(N5/4),目前尚未被证明.
Sedgewick增量序列:
- {1, 5, 19, 41, 109, ...},该序列中的项或者是94i-9 * 2i+1或者是4i-32i+1
- 这种增量的最坏复杂度为O(N4/3),平均复杂度为O(N7/6),但是均末被证明.
源码:
//希尔排序
ArrayList.prototype.shellSort = function(){
//1. 获取数组长度
var length = this.array.length;
//2. 初始化的增量
var gap = Math.floor(length/2);
//3. while循环(gap不断减小)
while (gap >= 1){
//4. 以gap为间隔,进行分组,对分组进行插入排序
for(var i = gap; i < length; i++){
var temp = this.array[i];
var j = i;
while (this.array[j - gap] > temp && j > gap -1){
this.array[j] = this.array[j - gap];
j -= gap;
console.log(i);
}
//5. 将j位置的元素赋值给temp
this.array[j] = temp;
}
//6. 增量变化gap / 2
gap = Math.floor(gap / 2);
}
}
快速排序
- 认识快速排序
- 希尔排序相当于插入排序的升级版,快速排序其实是我们学习过的最慢的冒泡排序的升级版
- 我们知道冒泡排序需要经过很多次交换,才能在一次循环中,将最大值放在正确的位置
- 而快速排序可以在次循环中(递归),找出某个元素的正确位置并且该元素之后不需要任何移动
- 快速排序最重要的思想是分而治之:
- 第一步:从其中选出了65.(其实可以是选出任意的数字,以65举个栗子)
- 第二步:我们通过算法将所有小于65的数字放在65的左边,将所有大于65的数字放在65的右边
- 第三步:递归的处理左边的数据.(此如你选择31来处理左侧),递归的处理右边的数据(比如选择75来处理右侧,当然选择81可能更合适)
快速排序的枢纽
- 在快速排序中有 一个很重要的步骤就是选取枢纽(pivot也有人称为主元)
- 第一种:直接选择第一个元素作为枢纽,但第一个作为枢纽在某些情况下,效率并不是特别高
- 第二种:使用随机数,但是随即函数本身就是一个耗性能的操作
- 第三种:比较优秀的解决方案取头、中尾的中位数口例如8,12,3的中位数就是8
源码:
//快速排序
//1. 选择枢纽
ArrayList.prototype.median = function(left,right){
//1. 取出中间的数字
var center = Math.floor((left + right) / 2);
//2. 交换数据
if (this.array[left] > this.array[center]) {
this.swap(left,center);
}
if(this.array[center] > this.array[right]){
this.swap(center,right);
}
if (this.array[left] > this.array[right]) {
this.swap(left,right);
}
//3. 将center换到right - 1的位置
this.swap(center,right-1);
return this.array[right - 1];
}
//2. 快速排序的实现
ArrayList.prototype.quickSort = function(){
this.quick(0,this.array.length - 1);
}
ArrayList.prototype.quick = function(left,right){
//1. 结束条件
if (left >= right) return ;
//2. 获取枢纽
var pivot = this.median(left,right);
//3. 定义变量,用于记录当前找到 的位置
var i = left;
var j = right - 1;
//4.开始进行交换
while(true){
while(this.array[++i] < pivot) {};
while(this.array[--j] > pivot) {};
if(i < j){
this.swap(i,j);
}else{
break;
}
}
//6. 将枢纽放置在i的位置
this.swap(i, right - 1);
//7. 分栏治之
this.quick(left, i - 1);
this.quick(i + 1, right);
}
快速排序的效率
- 快速排序的最坏情况效率
- 什么情况下会有最坏的效率呢?就是每次选择的枢纽都是最左边或者最后边的.
- 那么效率等同于冒泡排序.
- 而我们的例子可能有最坏的情况吗?是不可能的.因为我们是选择三个值的中位值.
- 快速排序的平均效率是0(N * logN).