最近在准备面试,翻看以前自己对于排序算法的实现,nima…当时脑子是进水了吗,写出来的代码缺东少西,而且再看竟然还忘了。。真是的! 还是重新实现一下吧。。
插入排序
思想: 从第二个数开始依次插入之前已经排好序的序列中。插入的过程是将这个元素和已经排好序的序列倒序比较,若是小于这个元素(从小到大排序)则后移排好序的元素,若是不小于则插入这个元素。
代码:
1 2 3 4 5 6 7 8 9 
  | function insert(arr) {     for(let i=1;i<arr.length;i++){         let j=i,temp = arr[i]        while( arr[--j]>temp){         arr[j+1] = arr[j]       }         arr[j+1] = temp     } } 
  | 
分析: 插入排序使用的是增量(incremental)方法:在排好子数组A[1…j-1]后,将A[j]插入,形成排好序的子数组A[1…j].
最坏情况: 输入数组是逆序排序。 最好O(n),最坏O(n^2),平均O(n^2)。
希尔排序
思想:希尔排序又称为缩小增量排序。希尔排序是插入排序的改进版。是依据设定的间隔序列,将数组分成若干个子序列然后分别执行插入排序。
步骤:            
- 选择一个增量序列t1,t2,..ti,tj,…tk, 其中 ti>tj,tk=1。(即最后一个的间隔是1)      
 
- 按增量序列个数k,对序列进行k趟排序。      
 
- 每趟排序,根据相应的增量ti,将待排序分割成若干长度为m的子序列,分别对各子序列进行直接插入排序。仅增量为1时,得到最后排好序的序列。
代码:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
  | function shellSort(arr) {     var len = arr.length,         temp,         gap = 1;     while (gap < len / 5) {                   gap = gap * 5 + 1;     }     for (gap; gap > 0; gap = Math.floor(gap / 5)) {                  for (var i = gap; i < len; i++) {             temp = arr[i];              for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {                 arr[j + gap] = arr[j];             }             arr[j + gap] = temp;         }     }     return arr; } 
  | 
 
 
分析: 最好最坏平均时间复杂度O(nlgn)。 不稳定
希尔排序不稳定的例子: 19 2 2 1 。 当步长为2时,第二个2会与19 交换,这样两个2的相对顺序会破坏
冒泡排序
思想:重复交换相邻的两个反序元素。      
代码:
1 2 3 4 5 6 7 8 9 10 
  | function bubbleSort(arr) {     for (var i = 0; i < arr.length - 1; i++) {         for (var j = 0; j < arr.length - 1 - i; j++) {             if (arr[j] > arr[j + 1]) {                 [arr[j],arr[j+1]] = [arr[j+1],arr[j]]             }         }     }     return arr; } 
  | 
分析: 冒泡的时间复杂度情况和插入相同。
改进冒泡排序
思想:设置标志,若是在某趟冒泡中未发生置换,则可以直接返回
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 
  | function bubbleSort(arr) {     for (var i = 0; i < arr.length - 1; i++) {         var flag = true;         for (var j = 0; j < arr.length - 1 - i; j++) {             if (arr[j] > arr[j + 1]) {                 [arr[j],arr[j+1]] = [arr[j+1],arr[j]]                 flag = false;             }         }         if (flag) {             return arr;         }     }     return arr; } 
  | 
选择排序
思想:第i趟就选择剩下的Arr.length-i个中最小或最大的元素和第i个元素互换。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 
  | function selectSort(arr) {     for (var i = 0; i < arr.length-1; i++) {            var min = i;         for (var j = i; j < arr.length; j++) {             if (arr[j] < arr[min]) {                 min = j;             }         }         if(i !== min){             [arr[i],arr[min]] = [arr[min],arr[i]]         }     }     return arr; } 
  | 
分析:  最好最坏与平均都是O(n^2). 因为选择排序每次总是要比较完才能确定下来最小(或最大)的那个元素 ,不稳定。
选择排序不稳定例子: 2 2 2 1。 1与第一个2互换导致2的相对顺序破坏
快排—O(n)空间复杂度
思想:快排是分治法的一种。分治法是将原问题划分成n个规模较小而结构与原问题相似的子问题。递归的解决这些子问题,然后再合并其结果,就得到了原问题的解。      
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 
  | function quickSort(arr) {     if (arr.length <= 1) {         return arr;     }     var num = Math.floor(arr.length / 2);     var end = arr.length -1;     [arr[num],arr[end]] = [arr[end],arr[num]] ;     var left = [];     var right = [];     for (var i = 0; i < end; i++) {         if (arr[i] < arr[end]) {             left.push(arr[i])         } else {             right.push(arr[i])         }     }     return quickSort(left).concat([arr[end]], quickSort(right)); } 
  | 
分析: 快排最好与平均情况都是O(nlgn),最坏的情况是O(n^2)。最坏情况:如果每一层递归上所作的都是最坏情况的划分。上面实现的快排会使得空间复杂度是O(n). 不稳定。
快排不稳定的例子: 1 2 2 2 3. 若选择第二个2作为划分值的时候,第一个2与第三个2会同时在第二个2的左边或者右边,这样也会破坏相对顺序。
快排—O(1)空间复杂度
思想: 使用partition 方法,每次得到子数组的基准元素所在的index,然后根据此index,进行划分,由于是在原地进行的操作,所以不需要进行拼接。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
  | function quickSort(arr, start, end) {     if (start < end) {         var q = partition(arr, start, end)         quickSort(arr, start, q-1)          quickSort(arr, q + 1, end)      }     return arr } function partition(arr, start, end) {     var value = arr[end]     var i = start - 1     for (var j = start; j < end; j++) {         if (arr[j] <= value) {             ++i;             [arr[i], arr[j]] = [arr[j], arr[i]]         }     }     [arr[i + 1], arr[end]] = [arr[end], arr[i + 1]]     return i + 1 } 
  | 
分析:  上面这种实现是在原地进行排序。空间复杂度是O(1)。
由空间复杂度为O(lgn)的快排得到 时间复杂度为线性的第i个顺序统计量
思想: 在一个由n个元素组成的集合中,第i个顺序统计量是该集合中第i小的元素。 求第i小的元素最小时间复杂度是O(n)。
 对于最小值与最大值可以通过n-1次比较得出最后结果。这是最快的方法。但是要同时得到最大与最小值,最快的方法是Math.floor(3/2*n)。 方法是每两对之间进行比较。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
  | function partition(arr, p, r) {     var num = arr[r];     var i = p - 1;     for (var j = p; j <= r - 1; j++) {         if (arr[j] <= num) {             ++i;             var temp = arr[i];             arr[i] = arr[j];             arr[j] = temp;         }     }     var temp = arr[i + 1];     arr[i + 1] = arr[r];     arr[r] = temp;     return i + 1; } function randomizedSelect(arr, p, r, i) {     if (p == r) {         return arr[p];     }     var index = partition(arr, p, r);     var k = index - p + 1;     if (i == k) {         return arr[index];     } else if (i < k) {         return randomizedSelect(arr, p, index - 1, i)      } else {         return randomizedSelect(arr, index + 1, r, i - k)      } } 
  | 
分析: 时间复杂度是O(n)
快排的随机化版本
思想:每次作比较的基准元素都是随机产生的。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 
  | function randomizedQuickSort(arr) {      if (arr.length <= 1) {          return arr;      }      var num = Math.floor(Math.random()*arr.length);      var numValue = arr.splice(num,1)[0];      var left = [];      var right = [];      for (var i = 0; i < arr.length; i++) {          if (arr[i] < numValue) {              left.push(arr[i])          } else {              right.push(arr[i])          }      }      return randomizedQuickSort(left).concat([numValue], randomizedQuickSort(right));  } 
  | 
分析: 大大减少了最坏情况的发生。
归并排序
思想: 归并排序也是分治法中的一种。归并排序中 会将两个排好序的子数组再进行合并成整个的数组。这个Merge的过程时间复杂度是O(n),可以类比成两摞排好序的牌合并成一摞,所以只需n次。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 
  | function mergeSort(arr, start, end) {     if (start < end) {                  var middle = Math.floor((start + end) / 2)         mergeSort(arr, start, middle)         mergeSort(arr, middle + 1, end)         merge(arr, start, middle, end)     }     return arr } function merge(arr, start, middle, end) {     var left = arr.slice(start, middle + 1)     left[left.length] = Infinity     var right = arr.slice(middle + 1, end + 1)     right[right.length] = Infinity     var i = 0, j = 0     for (var k = start; k <= end; k++) {         if (left[i] < right[j]) {             arr[k] = left[i]             i++         } else {             arr[k] = right[j]             j++         }     } } 
  | 
分析: 最好最坏平均情况的复杂度都是O(nlgn)。空间复杂度是O(n)。
堆排序
思想:堆数据结构是一种数组对象。可以被视为一颗完全二叉树。树中每个结点与数组中存放该结点值的那个元素对应。树除了最后一层外其余层都是填满的。表示堆的数组A是一个具有两个属性的对象:length[A]是数组中的元素个数,heap-size[A]是存放在A中的堆的元素个数。 即A[0…A.length-1]都可以包含有效值,但A[heap-size[A]-1]之后的元素都不属于相应堆。若树的根是A[0],则对于给定了某个结点的下标i,其父节点是Parent(i) = Math.floor(i/2) ,Left(i) = 2i+1, Right(i) = 2i+2.二叉堆有最大堆和最小堆。对于最大堆,除了根节点之外的每个结点i,都有 A[Parent(i)]>=A[i],最小堆是 A[Parent(i)]<=A[i]。在从小到大的堆排序算法中是使用最大堆,在从大到小的堆排序算法中可以使用最小堆。 最小堆通常在构造优先队列中使用。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 
  |     function heapSort(A) {         BUIDMAXHEAP(A);         for (var i = A.length - 1; i >= 1; i--) {             [arr[0], arr[i]] = [arr[i], arr[0]]             MAXHEAPIFY(A, 0, i);         }         return A;     }     function BUIDMAXHEAP(A) {                  var parent = Math.floor(A.length / 2 - 1);         for (var i = parent; i >= 0; i--) {             MAXHEAPIFY(A, i, A.length);         }     }          function MAXHEAPIFY(A, i, heapsize) {         var l = 2 * i + 1;         var r = 2 * i + 2;         var largest =i;         if (l <= heapsize-1 && A[l] > A[i]) {             largest = l;         }          if (r <= heapsize-1 && A[r] > A[largest]) {             largest = r;         }         if (largest != i) {             [arr[largest], arr[start]] = [arr[start], arr[largest]]             MAXHEAPIFY(A, largest,heapsize);         }     }   function MAXHEAPIFY(A, i, heapsize) {     var l = 2 * i + 1;     var r = 2 * i + 2;     var largest =i;     while(l<heapSize){         if (  A[l] > A[i]) {             largest = l;         }          if (r < heapsize && A[r] > A[largest]) {             largest = r;         }         if (largest != i) {             [arr[largest], arr[start]] = [arr[start], arr[largest]]             i = largest              l = 2*i + 1             r = 2*i + 2         }else{             break         }     } } 
  | 
分析: 
- MAX-Heapify时间复杂度是O(lgn)            
 
- Build-max-heap 以线性时间运行,可以在无序的输入数组基础上构建出最大堆           
 
- HeapSort 运行时间是O(nlgn),对一个数组进行原地排序。 最好最坏与平均时间复杂度都是O(nlgn),空间复杂度O(1)        
 
- 不稳定
 
堆排序不稳定的例子: 5 5 5 。 第一个5 会与最后一个5互换,这样就会破坏相对顺序
优先级队列
思想:  优先队列是一种用来维护由一组元素组成的集合S的数据结构,这一组元素中的每一个都有一个关键字key。最大优先级队列的一个应用是在一台分时计算机上进行作业调度。这种队列对要执行的各作业及他们之间的相对优先关系加以记录。当一个作业完成或被中断时,用Extract-max从所有等待的作业中,选择出具有最高优先级的作业。 最小优先级队列可被应用于基于事件驱动的模拟器中,在这种应用中,队列中的各项是要模拟的事件,每一个都有一个发生时间作为其关键字。
代码: 下面是基于上面的最大堆实现的最大优先级队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
  | function heapMaximum(arr) {     BUIDMAXHEAP(arr)     return arr[0]; } function heapExtractMax(arr,heapSize){     if(heapSize<1){return ;}      var max= arr[0];     arr[0] = arr[arr[heapSize-1]];     --heapsize;     MAXHEAPIFY(arr,0,heapSize); } function heapIncreaseKey(arr,i,key){   if(key<arr[i]){return false;}   arr[i]=key;   var parent = Math.floor(i/2);   while(i>0 && arr[parent]<A[i]){      var temp = arr[i];      arr[i] = arr[parent];      arr[parent] = temp;      i = parent;   } } function maxHeapInsert(arr,key,heapSize){    ++heapSize;    arr[heapSize-1] = -1*Number.MIN_VALUE;    heapIncreaseKey(arr,heapSize,key); } 
  | 
 
分析: 和堆排序情况相同
计数排序
思想: 计数排序假设n个输入元素中的每一个都是介于0到k之间的整数,当k=O(n)时,计数排序的运行时间是o(n)。计数排序的基本思想就是对每一个输入元素x,确定出小于x的元素个数,有了这一信息,就可以把x直接放到它在最终输出数组中的位置上。假定输入是数组A[1…n],另外还需要两个数组:存放排序结果的B[1…n]和提供临时存储区的C[0…k]。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
  | function countingSort(arr, k) {     var b=[], c = [];     for (var i = 0; i <= k; i++) {         c[i] = 0;     }          for (var i = 0; i < arr.length; i++) {         c[arr[i]] = c[arr[i]] + 1;     }          for (var i = 1; i <= k; i++) {         c[i] = c[i] + c[i - 1];     }     for (var i = arr.length - 1; i >= 0; i--) {         b[c[arr[i]]-1] = arr[i];          c[arr[i]] = c[arr[i]] - 1;     }     return b; } 
  | 
分析: 计数排序假设n个输入元素中的每一个都是介于0到k之间的整数,当k=O(n)时,计数排序的运行时间是o(n)。计数排序一个关键的特点就是稳定性。计数排序经常用作基数排序算法的一个子过程。计数排序的稳定性对于基数排序的正确性来说,是非常关键的。
基数排序
思想:给定n个d位数,第1位是最低位,第d位是最高位。
1 2 3 4 
  | RadixSort(arr,d){     for i from 1 to d         so use a stable sort to sort array A on digit i } 
  | 
 
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 
  | function radixSort(arr, maxDigit) {     var mod = 10;     var dev = 1;     var bucket = [];     for (var i = 0; i < maxDigit; i++ , dev *= 10, mod *= 10) {         for (var j = 0; j < arr.length; j++) {             var temp = parseInt((arr[j] % mod) / dev);             if (bucket[temp] == null) {                 bucket[temp] = [];             }             bucket[temp].push(arr[j]);         }         var pos = 0;         for (var j = 0; j < bucket.length; j++) {             if (bucket[j] != null) {                 while (temp = bucket[j].shift()) {                     arr[pos++] = temp;                 }             }         }     }     return arr; } 
  | 
分析:基数排序适用于范围比较小,并且>=0。 给定n个d位数,每一个数位可以取k种可能的值,如果所用的稳定排序需要使用o(n+k)的时间,基数排序算法能以o(d*(n+k)的时间进行排序
桶排序
思想:当桶排序的输入符合均匀分布时,可以将区间[0,1)划分成n个相同大小的子区间,或称为桶。然后将n个输入数分布到各个桶中去。然后对各个桶中的数据进行排序,最后按次序将各个桶中的数据列出来
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 
  | function bucketSort(array, num) {     if (array.length <= 1) {         return array;     }     var len = array.length, buckets = [], result = [], min = max = array[0], regex = '/^[1-9]+[0-9]*$/', space, n = 0;     num = num || ((num > 1 && regex.test(num)) ? num : 10);     for (var i = 1; i < len; i++) {         min = min <= array[i] ? min : array[i];         max = max >= array[i] ? max : array[i];     }     space = (max - min + 1) / num;     for (var j = 0; j < len; j++) {         var index = Math.floor((array[j] - min) / space);         if (buckets[index]) {                var k = buckets[index].length - 1;             while (k >= 0 && buckets[index][k] > array[j]) {                 buckets[index][k + 1] = buckets[index][k];                 k--;             }             buckets[index][k + 1] = array[j];         } else {                 buckets[index] = [];             buckets[index].push(array[j]);         }     }     while (n < num) {         result = result.concat(buckets[n]);         n++;     }     return result; } 
  | 
分析: 桶排序最好是在符合均匀输入的时候使用,可以符合线性期望时间运行。
计数排序&基数排序&桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶
 
- 计数排序:每个桶只存储单一键值
 
- 桶排序:每个桶存储一定范围的数值
 
总结
一张来自网上很好看的图。。

在实际使用的时候是综合的排序,比如javascript中原生的sort 方法。在数据量比较小的时候使用 插入排序,在数据量比较大的时候使用O(n*lgn)的排序算法
一定要经常来复习和完善一下。不然还是不久就忘了。。。
参考文献
- 《算法导论》
 
- 十大排序算法实现(javascript)