About Sort

排序算法可以分为两类:

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破 O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

对于当前的我来讲,非比较类算法算是盲区。还好,比较类算法的确是高频考点。

  • 比较类排序算法有:

    • O(n^2): 冒泡、选择、插入、shell(希尔)
    • O(nlogN): 归并、快速、堆排序
  • 非比较类排序有:

基数、桶、计数排序

bubble(冒泡)

冒泡 时间复杂度O(n^2),属于最简单的排序方法之一了。
基本原理:
每一趟外侧循环的目的就是通过比较相邻元素,然后得到当次循环的最值。就像气泡似的,最值会向上冒出。

  1. function sort(arr) {
  2. for (let i = 0; i < arr.length; i += 1) {
  3. for (let j = 0; j < arr.length - i - 1; j += 1) {
  4. if (arr[j] > arr[j + 1]) {
  5. swapArrItem(arr, j, j + 1);
  6. }
  7. }
  8. }
  9. }
  10. // 交换
  11. function swapArrItem(arr, i1, i2) {
  12. let temp = arr[i1];
  13. arr[i1] = arr[i2];
  14. arr[i2] = temp;
  15. }

selection(选择)

时间复杂度O(n^2),属于最简单的排序方法之一了。
选择排序其实和冒泡还挺像,但是它没有那么多交换的过程。
核心原理就是:外侧循环表示当前位置,未来会填入剩下元素的最值。那么内层循环要做的事情就是找到这个最值的下标,然后把当前位置的元素和最值下标的元素互换位置。

  1. function sort(arr) {
  2. for (let i = 0; i < arr.length; i += 1) {
  3. let min = arr[i];
  4. let minIndex = i;
  5. for (let j = i + 1; j < arr.length; j += 1) {
  6. if (arr[j] < min) {
  7. minIndex = j;
  8. min = arr[j];
  9. }
  10. }
  11. if (i !== minIndex) {
  12. swapArrItem(arr, i, minIndex)
  13. }
  14. }
  15. }

insert(插入)

时间复杂度O(n^2)。
插入排序的思路在于,外侧循环相当要决定一个分界点,在这个分界点的一边是已经有序的集合,另一半是待排序的集合。
那么接下来要做的事情就是分界点无序的一边取元素,安置到有序的一端。
外侧循环做的事情就是一次决定分界点的位置,内层循环要做的事情就是用分界点毗邻的无序元素,一个个对比分界点毗邻的有序元素,从而找到自己合适的位置。

  1. function sort(arr) {
  2. for (let i = 1; i < arr.length; i += 1) {
  3. const curValue = arr[i];
  4. let j = i - 1;
  5. let goIndex = i;
  6. while (j >= 0) {
  7. if (curValue < arr[j]) {
  8. arr[j + 1] = arr[j]
  9. goIndex = j;
  10. j -= 1;
  11. } else {
  12. break;
  13. }
  14. }
  15. arr[goIndex] = curValue;
  16. }
  17. }

merge(归并)

时间复杂度是O(logN)。
这个排序算法的思想是,对数组进行分割,当分割成最小单位的时候重新合并成有序数组,然后有序数组之间再重新生成新的有序数组。这个排序算法体现的思想其实是分治。
是一个拆封再拆封,直到拆分成最小单元,然后从最小单元开始合并,直到全部元素合并的一个递归过程。
时间复杂度nlogN,是因为拆分的过程类似二分,这种对半的过程很显然,都是logN。但是在其内部的合并过程的复杂度是n。
下面是最简单的实现了。

  1. function sort(arr) {
  2. function sliceMerge(array) {
  3. console.log(array);
  4. if (array.length === 1) {
  5. return array;
  6. }
  7. // 先分割 分割的逻辑对半砍,这就决定了时间复杂度中肯定有logN
  8. const midIndex = Math.floor(array.length / 2);
  9. const leftArr = array.slice(0, midIndex);
  10. const rightArr = array.slice(midIndex, array.length);
  11. // 递归调用
  12. const leftRes = sliceMerge(leftArr);
  13. const rightRes = sliceMerge(rightArr);
  14. const merged = [];
  15. // while的个数决定了时间复杂度中有n
  16. while (leftRes.length > 0 || rightRes.length > 0) {
  17. if (leftRes[0] && rightRes[0]) {
  18. if (leftRes[0] <= rightRes[0]) {
  19. merged.push(leftRes.shift());
  20. } else {
  21. merged.push(rightRes.shift());
  22. }
  23. } else if (leftRes[0] && !rightRes[0]) {
  24. merged.push(leftRes.shift());
  25. } else {
  26. merged.push(rightRes.shift());
  27. }
  28. }
  29. return merged;
  30. }
  31. const res = sliceMerge(arr);
  32. // 改变原来数组
  33. res.forEach((item, index) => {
  34. arr[index] = item;
  35. });
  36. }

quick(快速)

快速排序的时间复杂度也是nlogN。
基本思想是,随便在数组中找一个下标,遍历其余元素,将比下标对应的元素大的放在一起,比下标小的放在一起,然后再对两个集合再递归做这样的操作。最终return出[…leftSortRes, tagValue, …rightSortRes] 确定位置的数组。
下面是一个比较简单,但是容易看懂的实现:

  1. function sort(arr) {
  2. function departSort(array) {
  3. if (array.length <= 1) {
  4. return array;
  5. }
  6. // 随机找一个位置,为了简单期间就第一个位置了
  7. const tagIndex = 0;
  8. const tagValue = array[tagIndex];
  9. const lessThanArr = []; // 比tag小的集合
  10. const moreThanArr = []; // 比tag大的集合
  11. for (let i = 1; i < array.length; i += 1) {
  12. if (array[i] <= tagValue) {
  13. lessThanArr.push(array[i]);
  14. } else {
  15. moreThanArr.push(array[i]);
  16. }
  17. }
  18. const leftSortRes = departSort(lessThanArr);
  19. const rightSortRes = departSort(moreThanArr);
  20. return [...leftSortRes, tagValue, ...rightSortRes];
  21. }
  22. const res = departSort(arr);
  23. // 改变原来数组
  24. res.forEach((item, index) => {
  25. arr[index] = item;
  26. });
  27. }