排序算法总结

196 阅读4分钟

以下为我整理的有关排序的知识,重点是用JS实现的几种常用排序算法,还在不断更新中:

JavaScript中的 sort( ) 方法

sort()方法用于对JS的数组的元素进行排序,该方法会直接改变原始数组。

let a=[40,5,6,23,4]
a.sort()
console.log(a)  // [23, 4, 40, 5, 6]  ???
  • 当数组长度小于等于10的时候,采用插入排序,大于10的时候,采用快速排序。
  • 默认排序顺序为按字母升序。如果处理字符串类型的数组直接调用就可以,数值型可能会返回错误。默认数字"23"将排在"4"前面。
    let fruits = ["Banana""Orange""Apple""Mango"];
    fruits.sort();                // 字母升序
    console.log(fruits);          // ["Apple", "Banana", "Mango", "Orange"]
    
  • 若要使用数字排序,必须在sort()内传入一个函数作为参数来指定升序/降序。
    let a= [40,5,6,23,4];
    a.sort((a,b)=>{return a-b}); // 数字升序
    console.log(a)               // [4, 5, 6, 23, 40] 
    

排序算法的评价指标

  • 执行时间:主要消耗在比较移动上,用时间复杂度来衡量,理想情况为O(logn)
  • 辅助数组:除去待排序数组所占空间之外,需要的额外存储空间,用空间复杂度来衡量,理想情况下为O(1)

1 插入排序 InsertSort

排序过程:初始序列只有a[0],从a[1]到a[n-1],按照顺序每次选择一个元素插入之前的序列,使这个序列仍是有序的。

const a=[3,5,2,4,7,1,9,8,6];

元素个数为n,执行n-1

初始a[0]不用比较   (3) 5  2  4  7  1  9  8  6   下一个待比较元素 5 
i=1 第一趟比较后: (3  5) 2  4  7  1  9  8  6   下一个待比较元素 2
i=2 第二趟比较后: (2  3  5) 4  7  1  9  8  6   下一个待比较元素 4
i=3 第三趟比较后: (2  3  4  5) 7  1  9  8  6   下一个待比较元素 7
i=4 第四趟比较后: (2  3  4  5  7) 1  9  8  6   下一个待比较元素 1
i=5 第五趟比较后: (1  2  3  4  5  7) 9  8  6   下一个待比较元素 9
i=6 第六趟比较后: (1  2  3  4  5  7  9) 8  6   下一个待比较元素 8
i=7 第七趟比较后: (1  2  3  4  5  7  8  9) 6   下一个待比较元素 6
i=8 第八趟比较后: (1  2  3  4  5  6  7  8  9)  全部排序完成
function InsertSort(a,n){
  let i,j;
  for(i=1;i<a.n;i++){  // a[i] 就是每次选取的比较元素,从a[1]到a[n-1]遍历
    let temp=a[i];
    for(j=i-1;j>=0;j--){    // a[j] 要从当前 a[i-1] 遍历 a[0],与当前temp进行比较
      if(temp<a[j]){        // 如果原排序数组 [1,3,4]  temp 为 2 那么要把3,4依次往后挪  
        a[j+1]=a[j];
      }else {               //遇到第一个比 temp 小的,就停止比较
        break;
      }
    }
    a[j+1]=temp;           //把 temp 插入到 a[j+1] 位置
  }
  return a;
}

const a=[3,5,2,4,7,1,9,8,6];
console.log(InsertSort(a,a.length));  //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
  • 时间复杂度:O(n^2) 两层循环,同最坏。外层一定要执行n-1趟,内层要最好情况比较一次,不移动,最差要比较i次,移动i+1次。
  • 空间复杂度:O(1) 仅用到一个temp变量存储每轮要比较的a[i]
  • 排序算法稳定

2 冒泡排序 BubbleSort

排序过程如下:比较a[0]与a[1]大小,若为逆序,则交换两个元素;比较a[1]与a[2]大小,若为逆序,则交换两个元素......这样第一趟后就使得最大的数在在其正确的位置a[n-1]上。

元素个数为n,执行n-1

初始状态: 3  5  2  4  7  1  9  8  6 
第一趟后: 3  5  2  4  7  1  8  6 (9)   找到了第1大的元素
第二趟后: 3  5  2  4  7  1  6 (8  9)   找到了第2大的元素
第三趟后:  3  5  2  4  1  6 (7  8  9)   ...
第四趟后:  3  5  2  4  1 (6  7  8  9)
第五趟后:  3  2  4  1 (5  6  7  8  9)
第六趟后:  3  2  1 (4  5  6  7  8  9)
第七趟后:  2  1 (3  4  5  6  7  8  9)
第八趟后:  1 (2  3  4  5  6  7  8  9)  找到了第8大的元素,结束排序
function BubbleSort(a,n){
  for(let i=0;i<n;i++){  //控制比较轮次,一共 n-1 趟
    for(let j=i+1;j<n-1-i;j++){
      if(a[i]>a[j]){
        let temp=a[i];
        a[i]=a[j];
        a[j]=temp;
      }
    }
  }
  return a;
}

const a=[3,5,2,4,7,1,9,8,6];
console.log(BubbleSort(a,a.length));
  • 时间复杂度:O(n^2) 两层循环,同最坏
  • 空间复杂度:O(1) 仅在交换a[i]与a[j]时用到一个temp变量
  • 排序算法稳定

3 快速排序 QuickSort

每趟选择一个标兵pivot(通常取该数组段的第一个元素),暂存它的值temp,并设定两个指针ij,分别指向该数组段的首尾(a[0]a[n-1]),如果a[i]<a[j],那么就执行i++或者j--(pivot被哪个指针指向,就移动另一个指针),如果a[i]>a[j],那么就swap(a[i],a[j]),并继续移动指针,等到i>j时,停止以上操作。

元素个数为n,执行的趟数取决于递归树的深度。

初始状态: 3  5  2  4  7  1  9  8  6   选取下一趟pivot为3
第一趟后:(1  2) 3 (4  7  5  9  8  6)  标兵3的位置一定正确,对两侧数组选取标兵为1  4
第二趟后: 1 (2) 3  4 (7  5  9  8  6)  标兵1 3 4位置一定正确,左侧数组只剩一个元素,结束比较,右侧选取标兵为7
第三趟后: 1  2  3  4 (6  5) 7 (8  9)  继续选取标兵 6  8
第四趟后: 1  2  3  4  6 (5) 7  8 (9)  所有数组段内的元素都有序,结束比较 

每趟选择的标兵pivot会将数组分成两段,对每段数组的排序可以继续选取标兵,然后递归调用该方法去对标兵进行排序,递归的终止条件为 i>j时,就停止排序,此时的数组已经排好序了。

function QuickSort(a,left,right){
  if(left>right){         //递归终止条件
    return;
  }
  let i=left,j=right;    //设定两个指针i j
  let pivot=a[left];
  while(i<j){
    if(a[i]<=a[j]){      //这一步必须有=
      if(a[i]===pivot){
        j--;
      }else {
        i++;
      }
    }else {
      let tmp=a[i];
      a[i]=a[j];
      a[j]=tmp;
    }
  }
  let pivotIndex;        //标兵的index值
  for(pivotIndex=left;pivotIndex<=right;pivotIndex++){
    if(a[pivotIndex]===pivot){
      break;
    }
  }
  QuickSort(a,left,pivotIndex-1);  //递归调用pivot左边的数组进行排序
  QuickSort(a,pivotIndex+1,right); //递归调用pivot右边的数组进行排序
}

const a=[3,5,2,4,7,1,9,8,6];
QuickSort(a,0,a.length-1);  //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
console.log(a);
  • 时间复杂度: 平均O(nlogn) ,同最好
    • 最好的情况O(nlogn):因为每一趟中确定pivotIndex的时间复杂度为O(n),若每一趟后pivot能将左右两段的数据分割成长度大致相同,那么需要 logn 趟就能完成所有数字的排序,递归树如下:
              1000 个 数
              /         \
            500  pivot  500
           /   \       /   \
         250   250   250   250
         / \   / \   / \   / \
      ... ... ... ... ... ... ...
    
    • 最坏的情况O(n^2):递归树变成单支树,即每次选出的pivot只能划分得到一个比上一次少一个元素的数组段,这样必须经过n-1趟才能完成排序。
                  1000 个 数
                 /         \
               998  pivot   1
              /   \     
            997    1   
            / \ 
          ...  1         
    
    
  • 空间复杂度:最好O(logn) 最坏 O(n) 每次递归时,交换a[i]与a[j]需要一个temp变量去暂存数据。
  • 排序算法不稳定,因为存在交换 此外,还有一些简便的快排写法:
function QuickSort(a){
  if(a.length <= 1){
      return a;
  }
  const left = [],right = [];
  const pivotIndex = parseInt(a.length / 2);
  const pivot= a[pivotIndex];
  for(var i = 0 ; i < a.length ; i++){
      if(i===pivotIndex) 
        continue;
      if( a[i] < pivot){
        left.push(a[i])
      }else{
        right.push(a[i]);
      }
  }
  return QuickSort(left).concat([pivot],QuickSort(right));
}

const a=[3,5,2,4,7,1,9,8,6];
console.log(QuickSort(a));    //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

4 选择排序 SelectionSort

  • 第一趟从a[0]开始,通过n-1次的比较,选出最小的数a[k],交换a[0]与a[k]。
  • 第二趟从a[1]开始,通过n-2次的比较,选出最小的数a[k],交换a[1]与a[k]。
  • ...
  • n-1趟从a[n-2]开始,通过1次的比较,选出最小的数a[k],交换a[n-2]与a[k]。 元素个数为n,执行n-1趟。
初始状态: 3  5  2  4  7  1  9  8  6 
第一趟后:(1) 5  2  4  7  3  9  8  6   交换13
第二趟后:(1  2) 5  4  7  3  9  8  6   交换25
第三趟后:(1  2  3) 4  7  5  9  8  6   交换35
第四趟后:(1  2  3  4) 7  5  9  8  6   比较后无需交换
第五趟后:(1  2  3  4  5) 7  9  8  6   交换57
第六趟后:(1  2  3  4  5  6) 9  8  7   交换67
第七趟后:(1  2  3  4  5  6  7) 8  9   交换79
第八趟后:(1  2  3  4  5  6  7  8) 9   比较后无需交换
function SelectionSort(a,n){
  for(let i=0;i<n;i++){      //比较 n-1 趟
    let minIndex=i,min=a[i];
    for(let k=i+1;k<n;k++){  //k从i+1位遍历到n-1位
      if(a[k]<min){
        min=a[k];
        minIndex=k;          //记录最小值的索引
      }
    }
    let temp=a[i];           //交换a[i]与a[minIndex]的值
    a[i]=a[minIndex];
    a[minIndex]=temp;
  }
  return a;
}
const a=[3,5,2,4,7,1,9,8,6];
console.log(SelectionSort(a,a.length));    //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
  • 时间复杂度:O(n^2) 两层循环,同最坏。外层一定要执行n-1趟,内层要最好情况每趟都不交换,最差情况每趟都要比较完交换。
  • 空间复杂度:O(1) 仅用到temp、minIndex等变量。
  • 排序算法不稳定,因为存在交换。

总结

排序方法时间复杂度空间复杂度稳定性适用场景
插入排序平均O(n^2) 最好O(n) 最坏O(n^2)O(1)稳定基本有序,n小
冒泡排序平均O(n^2) 最好O(n) 最坏O(n^2)O(1)稳定基本有序,n小
快速排序平均O(nlogn) 最好O(nlogn) 最坏O(n^2)O(logn)不稳定无序,n较大
选择排序平均O(n^2) 最好O(n) 最坏O(n^2)O(1)不稳定基本有序,n小

排序应用

寻找第K大 —— 快速排序 + 递归

利用快速排序的思想寻找数组中的第k大元素,数组内有重复数字。

  • 每次设置一个标兵pivot,按照降序排序,将大于pivot的移到左边,小于pivot的移到右边,获得它的索引pivotIndex
  • 如果每次找到的 pivot 左边刚好有K-1个比它大的,那么该元素就是第K大,即pivotIndex===k-1
  • 如果pivotIndex>k-1,说明左边的元素比K-1多,说明第K大在其左边,不用管pivot右边,直接递归进入左边。
  • 如果pivotIndex<k-1,说明它左边的元素比K-1少,那第K大在其右边,不用管pivot左边,直接递归进入右边。
  • 记得设置终止条件,即left>right时返回。
function findKth(a,n,k) {
  return quickSort(a,0,n-1,k);
}
function quickSort(a,left,right,k){
  if(left>right){
    return;
  }
  let pivot=a[left];
  let index=left;
  let i=left,j=right;
  while(i<j){
    if(a[i]>=a[j]){         //注意这里
      if(a[i]===pivot){
        j--;
      }else {
        i++;
      }
    }else {
      index=(index===i)?j:i;   //标兵的index一直在变
      let temp=a[i];
      a[i]=a[j];
      a[j]=temp;
    }
  }
  if(index===k-1){
    return a[index];
  }else if(index>k-1){
    return quickSort(a,left,index-1,k);
  }else {
    return quickSort(a,index+1,right,k);
  }
}
let a=[1,3,5,2,2];
console.log(findKth(a,a.length,3));    // 2

还在不断更新中~