十大排序算法总结

173 阅读5分钟

十大排序算法

排序算法平均时间复杂度最坏时间复杂度最好时间复杂度空间复杂度稳定性
冒泡排序(Bubble Sort)O(n^2)O(n^2)O(n)O(1)稳定
选择排序(Selection Sort)O(n^2)O(n^2)O(n^2)O(1)不稳定
插入排序(Insertion Sort)O(n^2)O(n^2)O(n)O(1)稳定
希尔排序(Shell Sort)取决于间隔序列取决于间隔序列取决于间隔序列O(1)不稳定
归并排序(Merge Sort)O(n log n)O(n log n)O(n log n)O(n)稳定
快速排序(Quick Sort)O(n log n)O(n^2)O(n log n)O(log n)~O(n)不稳定
堆排序(Heap Sort)O(n log n)O(n log n)O(n log n)O(1)不稳定
计数排序(Counting Sort)O(n + k)O(n + k)O(n + k)O(k)稳定
桶排序(Bucket Sort)O(n + k)O(n^2)O(n)O(n + k)稳定
基数排序(Radix Sort)O(k * n)O(k * n)O(k * n)O(n + k)稳定

冒泡排序

// 冒泡排序
// 核心思路:比较相邻的两个数,如果前面的数比后面的数大,则交换位置,每一轮得到一个最大的数排在最后
// 时间复杂度最坏的情况: O(n^2)
// 时间复杂度平均的情况: O(n^2)
// 空间复杂度: O(1)
function bubbleSort(arr: number[]): number[] {
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - i; j++) {
      // 如果前面的数比后面的数大,则交换位置
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

选择排序

// 选择排序
// 核心思路:每一趟从待排序的数据元素中选出一个最大值排在最后,直到全部待排序的数据元素排完
// 时间复杂度最坏的情况: O(n^2)
// 时间复杂度平均的情况: O(n^2)
// 空间复杂度: O(1)
function selectSort(arr: number[]): number[] {
  for (let i = 0; i < arr.length; i++) {
    let maxNumIndex = 0;
    for (let j = 1; j < arr.length - i; j++) {
      if (arr[j] > arr[maxNumIndex]) {
        maxNumIndex = j;
      }
    }
    // 最大数所在位置 和 目前排序数列最后一位 交换位置
    [arr[maxNumIndex], arr[arr.length - i - 1]] = [
      arr[arr.length - i - 1],
      arr[maxNumIndex],
    ];
  }
  return arr;
}

插入排序

// 插入排序
// 核心思路:类似于打扑克牌,每次从未排序的数列中取出一个数,插入到已排序数列的合适位置
// 时间复杂度最坏的情况: O(n^2)
// 时间复杂度平均的情况: O(n^2)
// 空间复杂度: O(1)
const insertSort = (arr: number[]): number[] => {
  for (let i = 1; i < arr.length; i++) {
    let preIndex = i - 1;
    let nextIndex = i;
    while (arr[nextIndex] < arr[preIndex] && preIndex >= 0) {
      [arr[nextIndex], arr[preIndex]] = [arr[preIndex], arr[nextIndex]];
      nextIndex--;
      preIndex--;
    }
  }
  return arr;
};

希尔排序

const insertSortByStep = (
  arr: number[],
  offset: number,
  step: number
): number[] => {
  // 从第1个数开始,每次往后移动step位
  for (let i = step + offset; i < arr.length; i = i + step) {
    let nextIndex = i;
    let preIndex = nextIndex - step;
    while (arr[nextIndex] < arr[preIndex] && preIndex >= offset) {
      // 交换
      [arr[preIndex], arr[nextIndex]] = [arr[nextIndex], arr[preIndex]];
      nextIndex -= step; // 下标往前移动step位
      preIndex -= step; // 下标往前移动step位
    }
  }
  return arr;
};

// 希尔排序
// 核心思路: 根据步长分组,然后对每一组进行插入排序
// 时间复杂度最坏的情况: O(n^2)
// 时间复杂度平均的情况: O(n^1.3)
// 空间复杂度: O(1)
const shellSort = (arr: number[]): number[] => {
  for (
    let step = Math.round(arr.length / 2);
    step > 0;
    step = Math.floor(step / 2)
  ) {
    for (let offset = 0; offset < step; offset++) {
      insertSortByStep(arr, offset, step);
    }
  }
  return arr;
};

归并排序

const merge = (left: number[], right: number[]) => {
  let i = 0;
  let j = 0;
  const res: number[] = [];
  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      res.push(left[i]);
      i++;
    } else {
      res.push(right[j]);
      j++;
    }
  }
  while (i < left.length) {
    res.push(left[i]);
    i++;
  }
  while (j < right.length) {
    res.push(right[j]);
    j++;
  }
  return res;
};

// 归并排序
// 核心思路:把数组分成两个子数组,分别对它们进行排序,然后把这两个子数组合并成一个有序的数组
// 时间复杂度最坏: O(nlogn)
// 时间复杂度平均: O(nlogn)
// 空间复杂度: O(n)
const mergeSort = (arr: number[]): number[] => {
  if (arr.length <= 1) {
    return arr;
  }
  const mid = Math.floor(arr.length / 2);
  const leftArr = arr.slice(0, mid);
  const rightArr = arr.slice(mid);
  const left = mergeSort(leftArr);
  const right = mergeSort(rightArr);
  return merge(left, right);
};

快速排序

// 快速排序
// 核心思路:选取一个基准值,把小于基准值的放到左边,大于基准值的放到右边,然后递归
// 时间复杂度最坏的情况: O(n^2)
// 时间复杂度平均的情况: O(nlogn)
// 空间复杂度: O(n)
export const quickSort = (arr: number[]): number[] => {
  if (arr.length <= 1) return arr;
  const val = arr[0];
  const left: number[] = [];
  const right: number[] = [];
  for (let i = 1; i < arr.length; i++) {
    arr[i] < val ? left.push(arr[i]) : right.push(arr[i]);
  }
  return [...quickSort(left), val, ...quickSort(right)];
};
// 快速排序
// 核心思路:选取一个基准值,把小于基准值的放到左边,大于基准值的放到右边,然后递归
// 时间复杂度: O(nlogn)
// 该版本不会创建新的数组,而是在原数组上进行操作,减少了空间复杂度
const quickSortPlus = (
  arr: number[],
  start: number = 0,
  end: number = arr.length - 1
) => {
  if (end - start < 1) return arr;
  // 选取基准值
  const val = arr[start];
  // 基准值的下标
  let idx = start;
  for (let i = start + 1; i <= end; i++) {
    // 把小于基准值的放到第一个,并且前面的值都后移一位
    if (arr[i] < val) {
      const temp = arr[i];
      let j = i;
      while (j > start) {
        arr[j] = arr[j - 1];
        j--;
      }
      arr[start] = temp;
      idx++;
    }
  }
  quickSortPlus(arr, start, idx - 1);
  quickSortPlus(arr, idx + 1, end);
  return arr;
};

堆排序

// 维护堆
const heapify = (arr: number[], i: number, len: number) => {
  const left = 2 * i + 1;
  const right = 2 * i + 2;
  let largest = i;
  if (left < len && arr[left] > arr[largest]) {
    largest = left;
  }
  if (right < len && arr[right] > arr[largest]) {
    largest = right;
  }
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    // 继续递归维护堆
    heapify(arr, largest, len);
  }
};

// 构建大顶堆  构建大顶堆的过程就是维护堆的过程 (node下标 <arr.length / 2, 因为大于这个下标没有子节点了,没必要再维护堆)
const buildMaxHeap = (arr: number[]): number[] => {
  const len = arr.length;
  for (let i = Math.floor(arr.length / 2); i >= 0; i--) {
    heapify(arr, i, len);
  }
  return arr;
};

// 堆排序
// 核心思路:构建大顶堆,然后把堆顶元素与最后一个元素交换,然后维护堆,重复这个过程
// 时间复杂度最坏: O(nlogn)
// 时间复杂度平均: O(nlogn)
// 空间复杂度: O(1)
const heapSort = (arr: number[]): number[] => {
  buildMaxHeap(arr);
  for (let i = arr.length - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]]; // 交换
    heapify(arr, 0, i);
  }
  return arr;
};

计数排序

// 计数排序
// 核心思路:将数字转换为数组的下标,统计每个数字出现的次数,再遍历新数组,将索引加上最小值,作为原数组的值
// 时间复杂度最坏: O(n+k) k为最大值和最小值的差值
// 时间复杂度平均: O(n+k)
// 空间复杂度: O(n+k)
const countSort = (arr: number[]): number[] => {
  // 1. 找出最大值和最小值
  let min = arr[0];
  let max = arr[0];
  for (let i = 0; i < arr.length; i++) {
    const cur = arr[i];
    if (cur < min) {
      min = cur;
    }
    if (cur > max) {
      max = cur;
    }
  }

  // 2. 创建一个新数组,长度为最大值和最小值的差值
  const newArr = new Array(max - min + 1).fill(0);

  // 3. 遍历原数组,将原数组的值作为新数组的索引,新数组的值作为出现次数
  for (let i = 0; i < arr.length; i++) {
    const cur = arr[i];
    newArr[cur - min]++;
  }

  // 4. 遍历新数组,将新数组的索引加上最小值,作为原数组的值
  const res: number[] = [];
  for (let i = 0; i < newArr.length; i++) {
    let cur = newArr[i];
    while (cur > 0) {
      res.push(i + min);
      cur--;
    }
  }
  return res;
};

桶排序

// 桶排序
// 核心思路:将要排序的数据分到桶里,每个桶里的数据再单独进行排序
// 时间复杂度最坏: O(n+k)    k为桶的数量
// 时间复杂度平均: O(n+k)
// 空间复杂度: O(n+k)
const bucketSort = (arr: number[]): number[] => {
  // 1.得到数列的最大值和最小值,并算出差值d
  let max = arr[0];
  let min = arr[0];
  for (let i = 0; i < arr.length; i++) {
    if (max < arr[i]) max = arr[i];
    if (min > arr[i]) min = arr[i];
  }
  const d = max - min;
  // 2.初始化桶
  const bucketNum = 10;
  const bucketArr: number[][] = [];
  for (let i = 0; i <= bucketNum; i++) {
    bucketArr[i] = [];
  }
  // 3.遍历原始数组,将每个元素按照大小比例放入不同的桶中
  for (let i = 0; i < arr.length; i++) {
    const index = Math.floor(((arr[i] - min) / d) * bucketNum);
    bucketArr[index].push(arr[i]);
  }

  // 4.对每个桶内部进行快速排序
  const res = bucketArr.map((item) => quickSort(item)).flat();
  return res;
};


const quickSort = (arr: number[]): number[] => {
  if (arr.length <= 1) return arr;
  const val = arr[0];
  const left: number[] = [];
  const right: number[] = [];
  for (let i = 1; i < arr.length; i++) {
    arr[i] < val ? left.push(arr[i]) : right.push(arr[i]);
  }
  return [...quickSort(left), val, ...quickSort(right)];
};

基数排序