算法(二):排序算法(中)

229 阅读2分钟

归并排序

归并排序的基本思想是将待排序数组分成若干个子数组,然后将相邻的子数组归并成一个有序数组,最后再将这些有序数组归并成一个整体有序的数组。这其实就是分治法,把大问题分解成小问题来解决。
归并排序的思路大致分为两步:

  1. 分解:使用递归算法实现分解。如果待排序数组长度为1,认为这个数组已经有序,直接返回;
  2. 合并:利用双指针合并两个有序数组,直到合并完所有数组然后返回即可。

image.png 先实现合并两个有序数组:

// 合并两个有序数组
function mergeSortedArr(a: number[], b: number[]) {
  let i = 0, j = 0;
  // 声明新数组
  const mergeArr: number[] = []
  while(i < a.length && j < b.length) {
    // 将较小的元素放入新数组
    a[i] < b[j] ? mergeArr.push(a[i++]) : mergeArr.push(b[j++]);
  }
  // 处理剩余元素
  if (i < a.length) mergeArr.push(...a.slice(i));
  if (j < b.length) mergeArr.push(...b.slice(j));
  return mergeArr;
}

然后是分解的部分:

function 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);
  // 合并
  return mergeSortedArr(mergeSort(leftArr), mergeSort(rightArr));
}

归并排序总结

时间复杂度:分解的操作是O(longn),合并的操作是O(n),所以总体的时间复杂度是O(nlongn)。由此可见归并排序的效率是非常高的,所以很多语言底层的排序算法都是使用归并排序。

快速排序(划分交换排序)

快速排序也是基于分治的思想,思路是将一个大数组分成两个小数组,然后递归的对两个小数组进行排序。具体点说是通过一个基准元素,将数组分成左右两部分,左部分的元素都小于等于基准元素,右部分的元素都大于基准元素,然后,对左右两部分分别进行递归调用快速排序,最终将整个数组排序。如下图所示:

image.png 拿图中的第一层到第二层来详细说明一下交换的步骤:首先把最后一个元素8定为基准元素,然后定义剩余元素的左右指针i(20)、j(6),从i开始向右找一个比8大的元素,找到20,然后停止,从j向左找一个比基准元素小的元素6,然后交换20和6。继续这个步骤从左向右找大于基准元素的元素和从右向左找一个比基准元素小的元素互换,直到j<i为止,到此还需要交换基准元素和i所对应的元素。到此基准元素左边一定比自身小,右边一定比自身大。后续重复此步骤处理左右两边的元素即可。具体实现:

// 快速排序
function quickSort(arr: number[]): number[] {
  // 定义递归函数
  function partition(left: number, right: number): void {
    // 递归结束条件
    if (left >= right) return;
    // 基准元素
    const pivot = arr[right];
    // 双指针
    let i = left;
    let j = right - 1;
    while (i <= j) {
      // 找比基准元素大的元素 这里不用处理指针边界 因为i为right时等于pivot
      while(arr[i] < pivot){
        i++
      }
      // 找比基准元素小的元素
      while(j >= left && arr[j] > pivot){
        j--
      }
      // 交换位置
      if (i <= j) {
        [arr[i], arr[j]] = [arr[j], arr[i]];
        i++;
        j--;
      };
    }
    // 将基准元素放到i的位置
    [arr[i], arr[right]] = [arr[right], arr[i]];
    // 递归基准元素左右两边的元素
    partition(left, i - 1);
    partition(i + 1, right);
  }
  partition(0, arr.length - 1);
  return arr;
}

快速排序总结

时间复杂度:最好情况下,每次的基准元素都交换到在正中间,那么需要进行O(logn)次递归,每次递归处理的时间复杂度是O(n),所以最终的时间复杂度是O(nlogn)。最坏的情况是每次基准元素都是最大或最小的,那么就需要递归n次,最终时间复杂度就是O(n^2)。最坏的情况基本不会出现,除非对一个有序的数组进行快速排序,但就算是这样,也可以采用随机取基准元素的方式来避免这种情况。另外需要注意的是,快速排序是原地排序,不需要额外的空间。
虽然归并排序和快速排序的时间复杂度都能达到O(logn),但是快速排序的O(nlogn)中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。