# 排序

# 稳定

  • 冒泡排序(Bubble Sort) — O(n²)
  • 插入排序(Insertion Sort)— O(n²)
  • 桶排序(Bucket Sort)— O(n); 需要 O(k) 额外空间
  • 计数排序 (Counting Sort) — O(n+k); 需要 O(n+k) 额外空间
  • 合并排序(Merge Sort)— O(nlogn); 需要 O(n) 额外空间
  • 二叉排序树排序 (Binary tree sort) — O(n log n) 期望时间; O(n²)最坏时间; 需要 O(n) 额外空间
  • 基数排序(Radix sort)— O(n·k); 需要 O(n) 额外空间

# 不稳定

  • 选择排序(Selection Sort)— O(n²)
  • 希尔排序(Shell Sort)— O(nlogn)
  • 堆排序(Heapsort)— O(nlogn)
  • 快速排序(Quicksort)— O(nlogn) 期望时间, O(n²) 最坏情况; 对于大的、乱数串行一般相信是最快的已知排序

# O(n^2)

  • 插入排序
  • 选择排序
  • 希尔排序
  • 冒泡排序

# O(nlogn)

  • 快排
  • 归并排序
  • 堆排序

# O(n)

  • 桶排序
  • 计数排序
  • 基数排序

# 详细

# 1. 冒泡排序(稳定)

原地排序、稳定排序。每次两两比较,大的放到后面。每一次遍历都会将最后一位“就位”。

  • 最好 O(n)
  • 最坏 O(n^2)
  • 平均 O(n^2)
function sort(arr = []) {
  const len = arr.length;
  // 外层,需要遍历的次数
  for (let i = 0; i < len - 1; i++) {
    // 内层,每次比较
    for (let j = i + 1; j < len; j++) {
      if (arr[i] > arr[j]) {
        // 大的放到后面
        [arr[i], arr[j]] = [arr[j], arr[i]];
      }
    }
  }
  return arr;
}

# 2. 快速排序(不稳定)

原理:分治 + 递归

随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作。

  • 最好 O(nlogn)
  • 最坏 O(n^2)
  • 平均 O(nlogn)
/**
 * 将数组 arr 分为两部分,前一部分整体小于后一部分
 */
function partition(arr, left, right) {
  // 交换数组最左元素与数组的中间元素
  let midIndex = ((left + right) / 2) >> 0;
  swap(arr, left, midIndex);
  // 基准元素
  const flagItem = arr[left];
  let i = left + 1,
    j = right;
  while (true) {
    while (i <= right && arr[i] < flagItem) {
      i++;
    }
    while (j >= left && arr[j] > flagItem) {
      j--;
    }
    if (i > j) {
      break;
    } else {
      swap(arr, i, j);
      i++;
      j--;
    }
  }
  swap(arr, left, j);
  return j;
}

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left >= right) {
    return;
  }
  const mid = partition(arr, left, right);
  quickSort(arr, left, mid - 1);
  quickSort(arr, mid + 1, right);
}

# 三向快速排序

类似快速排序,但是分为了左中右三个部分,中间部分为等于当前值的所有值。适用于有大量重复元素的排序。

const arr = [];
const str = 'SORTEXAMPLE';

for (let char of str) {
  arr.push(char);
}

function sort(arr, lo, hi) {
  if (hi <= lo) return;
  let lt = lo,
    i = lo + 1,
    gt = hi;
  let v = arr[lo];
  while (i <= gt) {
    if (arr[i] < v) swap(arr, lt++, i++);
    else if (arr[i] > v) swap(arr, i, gt--);
    else i++;
    console.log(arr);
  }
  sort(arr, lo, lt - 1);
  sort(arr, gt + 1, hi);
}

function swap(arr, a, b) {
  let x = arr[a];
  arr[a] = arr[b];
  arr[b] = x;
}

sort(arr, 0, arr.length - 1);

/**
 * 三向切分的快速排序指的是,将数组分为小于、等于和大于当前切分值的区域。
 * 切分点从开头字母开始
 * 擅长于有大量重复元素的情况
 */

# 3. 选择排序(不稳定)

选择排序,从头至尾扫描序列,找出最小的一个元素,和第一个元素交换,接着从剩下的元素中继续这种选择和交换方式,最终得到一个有序序列。

原地排序;不稳定排序

复杂度:

  • 最好 O(n^2)
  • 最坏 O(n^2)
  • 平均 O(n^2)
function sort(arr) {
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    let min = i;
    // 找到第 n 个最小值。 在 arr[i + 1, arr.length - 1] 中找最小值索引, i+1 代表有序的下一个数,我们默认第一个元素是最小的
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[min]) min = j;
    }
    // 每次循环, a[i] 位都将是未选择出的数据中的最小值
    if (min !== i) {
      [arr[i], arr[min]] = [arr[min], arr[i]];
    }
  }
  return arr;
}

# 4. 插入排序 (稳定)

插入排序的原理如下: 当前元素保存一个副本,依次向前遍历前面的元素是否比自己大,如果比自己大就直接把前一个元素赋值到当前元素的位置,当前某位置的元素不再比当前元素大的时候,将当前元素的值赋值到这个位置。

插入排序也是一个简单的排序算法,它的思想是,每次只处理一个元素,从后往前查找,找到该元素合适的插入位置,最好的情况下,即正序有序(从小到大),这样只需要比较 n 次,不需要移动。因此时间复杂度为 O(n) ,最坏的情况下,即逆序有序,这样每一个元素就需要比较 n 次,共有 n 个元素,因此实际复杂度为 O(n²) 。

插入排序:选中某个值,将这个值移动到 a[i - 1] < a[i] < a[i + 1] 位置。

复杂度:

  • 最好 O(n)
  • 最坏 O(n^2)
  • 平均 O(n^2)
// 插入排序
function insertSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    let temp = arr[i],
      j;
    for (j = i; j > 0 && arr[j - 1] > temp; j--) {
      arr[j] = arr[j - 1];
    }
    arr[j] = temp;
  }
  return arr;
}

# 5. 希尔排序 (缩小增量排序, 不稳定)

希尔排序:类似插入排序,但是对调的间隔扩大(插入排序是相邻元素对调位置的)。

按步长进行分组,组内直接插入,缩小增量再次进行此步骤,增量为 1 时相当于一次直接插入。

复杂度:最好 O(n) - 最坏 O(n^s 1<s<2) - 平均 O(n^1.3)

function sort(arr) {
  const len = arr.length;
  let h = 1;
  while (h < parseInt(len / 3)) {
    h = 3 * h + 1;
  }
  while (h >= 1) {
    for (let i = h; i < len; i++) {
      for (let j = i; j >= h && arr[j] < arr[j - h]; j -= h) {
        swap(arr, j, j - h);
        console.log(arr);
      }
    }
    h = parseInt(h / 3);
  }
  return arr;
}
// 希尔排序
// 从 start 开始,间隔为 gap 数组进行插入排序
function insertSort(a, start, gap) {
  let temp;
  for (let i = start; i < length; i++) {
    for (let j = i + gap; j < length; j += gap) {
      if (a[j] < a[i]) {
        temp = a[j];
        a[j] = a[i];
        a[i] = temp;
      } else {
        break;
      }
    }
  }
  return a;
}

function hillSort(a) {
  for (let gap = parseInt(a.length / 2); gap >= 1; gap = parseInt(gap / 2)) {
    insertSort(a, 0, gap);
  }
  return a;
}

# 6. 归并排序(稳定)

原理:两个有序序列的合并,方法:分治 + 递归

复杂度:最好 O(nlogn) - 最坏 O(nlogn) - 平均 O(nlogn)

归并排序:通过去中间值分割数组,然后再对子数组分割,分割到最小单位进行比较,然后逐个合并。还有一种(bottom to top)的方法是先将数组切分为若干段,然后进行合并排序。核心思想就是分而治之

// 归并排序。分治思想,从数组中间开始将数组分成两个部分,然后分别对两个部分进行排序
// 效率为O(n log n)
const b = [3, 4, 6, 1, 3, 6, 32, 45, 21, 12],
  length = b.length;

const mergeSort = arr => {
  const length = arr.length;
  if (length < 2) return arr;

  const midIndex = parseInt(length / 2);
  const left = arr.slice(0, midIndex),
    right = arr.slice(midIndex, length);

  return [...mergeSort(left), ...mergeSort(right)];
};

# V8 的排序

对于 JS 来说,数组长度大于 10 会采用快排,否则使用插入排序 源码实现 (opens new window) 。选择插入排序是因为虽然时间复杂度很差,但是在数据量很小的情况下和 O(N * logN)相差无几,然而插入排序需要的常数时间很小,所以相对别的排序来说更快。