微信图片_20211214160145.jpg
说来惭愧,到现在都还不能完全熟悉这十大排序算法,So It’s time to do it

上表

排序算法 平均时间复杂度 稳定性
冒泡排序 O(n^2) 稳定
选择排序 O(n^2) 不稳定
插入排序 O(n^2) 稳定
希尔排序 1.5*n 不稳定
归并排序 O(n*logn) 稳定
快速排序 O(n*logn) 不稳定
堆排序 O(n*logn) 不稳定
基数排序 O(d(n+r)) 稳定
拓扑排序
计数排序

稳定性

稳定性指序列中有相同的元素,经过排序后,其相对位置不变

冒泡排序

基本思想:相邻两个数进行比较,较大的数下沉,较小的数冒泡,即逆序便交换

过程:从后往前依次比较相邻两个数,每轮选出第 i 小元素放到对应位置

优化:设置标志位flag,每轮开始比较前将标志值设为false,若比较过程中发生交换,更改标志值为true。如果这轮比较结束标志值依旧为false,说明没有发生交换行为,元素顺序已排好,因此可直接返回。

时间复杂度:(n-1)+(n-2)+…+1,为等差数列 Sn=n(a1+an)/2=(n^2-n)/2,得时间复杂度为 O(n^2)

稳定性:在比较交换条件时,相同元素默认没有进行交换,所以该排序算法是稳定的

function bubbleSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    let flag = false;
    for (let j = arr.length - 1; j > i; j--) {
      if (arr[j - 1] > arr[j]) {
        [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
        flag = true;
      }
    }
    if (!flag) {
      break;
    }
  }
  console.log(arr);
}
let arr1 = [2, 4, 3, 2, 5, 6, 4, 7];
bubbleSort(arr1);

选择排序

基本思想:遍历序列,每次选出最小的元素与第 I 轮对应位进行交换

过程: 每轮遍历前建立最小值索引minIndex,通过比较更新索引值,第 i 轮遍历比较结束交换下标为 i 和minIndex的元素

时间复杂度:(n-1)+(n-2)+…+1,为等差数列 Sn=n(a1+an)/2=(n^2-n)/2,得时间复杂度为 O(n^2)

稳定性:序列[5(1),8,5(2),2,9],5(1)会和 2 交换,最终排序结果 5(1)和 5(2)的相对位置会被破坏,因此该算法不稳定。

function selectSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  console.log(arr);
}
let arr = [2, 4, 5, 7, 8, 3, 1, 6];
selectSort(arr);

插入排序

基本思想:采用减(减一)治思想,在要排序的一组数中,假定前 n-1 个数已经排好序,现在将第 n 个数插到前面的有序数列中,使得这 n 个数也是排好顺序的。如此反复循环,直到全部排好顺序。

过程:建立临时变量cur存储当前元素,从有序数组后面开始比较,移步赋值占空出来的位,直到找到第一个<=当前元素的元素,插入到其后面

时间复杂度:(n-1)+(n-2)+…+1,为等差数列 Sn=n(a1+an)/2=(n^2-n)/2,得时间复杂度为 O(n^2)

稳定性:由于比较等于时,是插入到后面,所以相对位置没有改变,因此该排序算法稳定。

function straightInsertionSort(arr) {
  for (let i = 1; i < arr.length; ++i) {
    let cur = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > cur) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = cur;
  }
  console.log(arr);
  return arr;
}
let arr1 = [89, 45, 68, 90, 29, 34, 17];
straightInsertionSort(arr1);

希尔排序

基本思想:是插入排序的优化,将序列按增量划分为子序列分别进行插入排序,逐渐将增量减小,并重复上述过程。直至增量为 1,

过程:增量incr的初始值为序列的长度,每轮划分子序列前将增量/2

优化:在对子序列进行插入排序时,注意当前元素cur的最小值要大于本序列的起始元素的索引 i

稳定性:序列[28,17(1),17(2),49],排序后 17(1)和 17(2)的相对位置会改变,该算法不稳定

function shellSort(arr) {
  let incr = arr.length;
  while (incr > 0) {
    incr = Math.floor(incr / 2);
    for (let i = 0; i < incr; i++) {
      for (let j = i + incr; j < arr.length; j += incr) {
        let cur = j;
        while (cur > i && arr[cur] < arr[cur - incr]) {
          [arr[cur], arr[cur - incr]] = [arr[cur - incr], arr[cur]];
          cur -= incr;
        }
      }
    }
  }
  console.log(arr);
}
let arr = [2, 4, 1, 5, 6, 8, 9];
shellSort(arr);

归并排序

基本思路:基于分治思想,将序列不断分半拆分,知道序列元素个数为 1 即有序,然后递归排序两个子序列,再将排好序的两个子数组合并

微信图片_20211214140112.jpg

过程:建立递归排序函数sort和合并函数merge,在合并过程中建立临时数组temp存放排序后的序列,再替换arr对应位置的元素

时间复杂度:设数列长为 N,将数列分开成小数列一共要 logN 步,每步都是一个合并有序数列的过程,时间复杂度可以记为 O(N),故一共为 O(N*logN)。

稳定性:排序后没有破坏相同元素的相对位置,故该排序算法稳定

function mergeSort(arr) {
  sort(arr, 0, arr.length - 1);
  console.log(arr);
}

function sort(arr, left, right) {
  if (left < right) {
    let mid = Math.floor((left + right) / 2);
    sort(arr, left, mid);
    sort(arr, mid + 1, right);
    merge(arr, left, mid, right);
  }
}

function merge(arr, left, mid, right) {
  let lf = left,
    le = mid,
    rf = mid + 1,
    re = right;
  let temp = [];
  while (lf <= le && rf <= re) {
    if (arr[lf] <= arr[rf]) {
      temp.push(arr[lf]);
      lf++;
    } else {
      temp.push(arr[rf]);
      rf++;
    }
  }
  while (lf <= le) {
    temp.push(arr[lf]);
    lf++;
  }
  while (rf <= re) {
    temp.push(arr[rf]);
    rf++;
  }
  arr.splice(left, right - left + 1, ...temp);
}

let arr = [2, 5, 3, 1, 6, 8, 4, 9];
let arr1 = [23, 65, 1, 234, 8, 65, 4, 9, 0, 23];
mergeSort(arr);
mergeSort(arr1);

快速排序

基本思想:

  • 先从数列中取出一个数作为 key 值,一般取第一位
  • 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
  • 对左右两个小数列重复第二步,直至各区间只有 1 个数。

过程:选取序列第一位元素为中轴,建立两个查找指针 i 和 j,i 从左往右扫描,j 从右往左扫描,i 遇到第一个>=中轴的元素便停下来,j 遇到第一个<=中轴的元素便停下来。当 i 和 j 都停下来时,交换 i 和 j 对应的元素,i 和 j 分别++,继续寻找,直到 j>i,便将 j 对应的元素和中轴进行交换,以交换后中轴的位置为界,继续递归划分子序列

微信图片_20211214143953.jpg

时间复杂度:O(n*logn)

稳定性:序列[6,8,5(1),5(2),7]排序后 5(1)和 5(2)的相对位置会改变,该排序算法不稳定

function quickSort(arr) {
  sort(arr, 0, arr.length - 1);
  console.log(arr);
}
function sort(arr, l, r) {
  if (l > r) {
    return;
  }
  let key = arr[l];
  let i = l + 1,
    j = r;
  while (i < j) {
    while (i < j && arr[i] < key) {
      i++;
    }
    while (arr[j] > key) {
      j--;
    }
    if (i < j) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
      j--;
    } else {
      break;
    }
  }
  // j>i
  [arr[l], arr[j]] = [arr[j], arr[l]];
  sort(arr, l, j - 1);
  sort(arr, j + 1, r);
}
let arr1 = [2, 5, 3, 7, 6, 9, 1, 4];
let arr2 = [34, 2, 2, 2, 1, 4, 5, 7, 89, 0, 45, 66];
quickSort(arr1);
quickSort(arr2);

堆排序

基本思想: 基于变治思想,默认续写为完全二叉树,通过调整序列来达到大顶堆或小顶堆的效果

过程:从最后一个非叶子节点开始调整,直到根节点。调整时需要考虑到下层,因此需要循环

a.假设给定无序序列结构如下

img

2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的 6 结点),从左至右,从下至上进行调整。

img

4.找到第二个非叶节点 4,由于[4,9,8]中 9 元素最大,4 和 9 交换。

img

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。

img

此时,我们就将一个无需序列构造成了一个大顶堆。

时间复杂度: 每次重新恢复堆的时间复杂度为 O(logN),共 N - 1 次重新恢复堆操作,再加上前面建立堆时 N / 2 次向下调整,每次调整时间复杂度也为 O(logN)。二次操作时间相加还是 O(N * logN)。

稳定性:比较过程中元素移动位置很错乱,该排序算法不稳定

function buildBigHeap(arr) {
  for (let i = arr.length / 2 - 1; i >= 0; i--) {
    adjustHeap(arr, i);
  }
}
function adjustHeap(arr, cur) {
  while (cur <= arr.length / 2 - 1) {
    let k = 2 * cur + 1;
    if (arr[k + 1] && arr[k + 1] > arr[k]) {
      k++;
    }
    if (arr[k] > arr[cur]) {
      [arr[k], arr[cur]] = [arr[cur], arr[k]];
    }
    cur = k;
  }
}

基数排序

稳定性:该算法是稳定的

10101001

9990

拓扑排序

基本思想:有向无环图

计数排序

基本思想