数组排序下(归并、快排)

61 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

上篇总结了冒泡排序、插入排序、选择排序,这些基础数组排序方法,下面总结一下如何进行归并排序,快速排序

这两个排序算法,是对‘分治‘思想的应用,什么是分治思想,即分而治之。 把一个大问题,拆分成若干小问题,依次求解出小问题的解,然后再将其解合并,成为大问题的解。 分三步:

  • 拆分大问题成若干小问题
  • 依次求解小问题
  • 合并小问题的解

下面我们一起来看看分治思想是如何帮助我们提升排序算法效率的

归并排序

归并排序是对分治思想的典型应用,它按照如下的思路对分治思想“三步走”的框架进行了填充:

  • 分解子问题:将需要被排序的数组从中间分割为两半,然后再将分割出来的每个子数组各分割为两半,重复以上操作,直到单个子数组只有一个元素为止。
  • 求解每个子问题:从粒度最小的子数组开始,两两合并、确保每次合并出来的数组都是有序的。(这里的“子问题”指的就是对每个子数组进行排序)。
  • 合并子问题的解,得出大问题的解:当数组被合并至原有的规模时,就得到了一个完全排序的数组

比如我们下面一个数组:

[8, 7, 6, 5, 4, 3, 2, 1]

不断拆分,首次拆分从中间分开成:

[8, 7, 6, 5,| 4, 3, 2, 1]

然后继续从中间拆分:

[8, 7,| 6, 5,| 4, 3,| 2, 1]

最后每个子数组内都只有一个元素了:

[8, |7,| 6,| 5,| 4,| 3,| 2,| 1]

接下来开始尝试解决每个子问题。将规模为1的子数组两两合并为规模为2的子数组,合并时确保有序,我们会得到这样的结果:

[7, 8,| 5, 6,| 3, 4,| 1, 2]

继续将规模为2的按照有序原则合并为规模为4的子数组:

[5, 6, 7, 8,| 1, 2, 3, 4]

最后将规模为4的子数组合并为规模为8的数组:

[1, 2, 3, 4, 5, 6, 7, 8]

通过上面的过程,我们可以总结出归并排序中的两个主要动作:

  • 分割
  • 合并

这两个动作是紧密关联的,分割是将大数组反复分解为一个一个的原子项,合并是将原子项反复地组装回原有的大数组。

function mergeSort(arr) {
  const len = arr.length
  // 递归边界
  if(len<=1) return arr
  // 计算分割点
  const mid = Math.floor(len/2)
  // 递归左分割数组,然后合并为有序数组
  const leftArr = mergeSort(arr.slice(0,mid))
  // 递归右分割数组,然后合并为有序数组
  const rightArr = mergeSort(arr.slice(mid,len))
  // 合并左右两个有序数组
  arr = mergeArr(leftArr,rightArr)
  return arr
}
// 合并两个有序数组(双指针)
function mergeArr(arr1,arr2) {
  // 初始化两个指针
  let i =0,j=0
  // 初始化结果数组
  const res = []
  // 缓存arr1的长度
  const len1 = arr1.length
  // 缓存arr2的长度
  const len2 = arr2.length
  // 合并两个子数组
  while(i< len1 && j<len2) {
    if(arr1[i]<arr2[j]) {
      res.push(arr1[i])
      i++
    } else {
      res.push(arr2[j])
      j++
    }
  }
  // 若其中一个子数组先被合并完全,则直接拼接另一个数组剩余的部分
  if(i<len1) {
    return res.concat(arr1.slice(i))
  } else {
    return res.concat(arr2.slice(j))
  }
}

归并排序的时间复杂度是 O(nlog(n))

快速排序

快速排序在基本思想上和归并排序是一致的,仍然坚持“分而治之”的原则不动摇。区别在于,快速排序并不会把真的数组分割开来再合并到一个新数组中去,而是直接在原有的数组内部进行排序。

核心思路: 快速排序会将原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。

下面描述一下排序过程

首先要做的事情就选取一个基准值。基准值的选择有很多方式,这里我们选取数组中间的值:

[5, 1, 3, 6, 2, 0, 7]
 ↑       基准       ↑

左右指针分别指向数组的两端。接下来我们要做的,就是先移动左指针,直到找到一个大于/等于基准值的值为止;然后再移动右指针,直到找到一个小于基准值的值为止。
首先我们来看左指针,5比6小,故左指针右移一位:

[5, 1, 3, 6, 2, 0, 7] 
    ↑    基准       ↑

继续对比,1比6小,继续右移左指针:

[5, 1, 3, 6, 2, 0, 7] 
       ↑ 基准       ↑

继续对比,3比6小,继续右移左指针,左指针最终指向了基准值:

[5, 1, 3, 6, 2, 0, 7] 
         基准       ↑ 
          ↑

此时由于 6===6,左指针停止移动。开始看右指针:
右指针指向7,7>6,故左移右指针:

[5, 1, 3, 6, 2, 0, 7] 
         基准    ↑ 
          ↑

发现 0 比 6 小,停下来,交换 6 和 0,同时两个指针共同向中间走一步:

[5, 1, 3, 0, 2, 6, 7] 
             ↑ 基准 
             ↑

此时 2 比 6 小,故右指针不动,左指针继续前进:

[5, 1, 3, 0, 2, 6, 7] 
             ↑ 基准 
           right↑ 
               left

此时右指针所指的值不大于 6,左指针所指的值不小于 6,故两个指针都不再移动。此时我们会发现,对于左指针所指的数字来说,它左边的所有数字都比它小,右边的所有数字都比它大(这里注意也可能存在相等的情况)。由此我们就能够以左指针为轴心,划分出一左一右、一小一大两个子数组:

[5, 1, 3, 0, 2] 
[6, 7]

针对两个子数组,重复执行以上操作,直到数组完全排序为止。这就是快速排序的整个过程。

// 快速排序入口
function quickSort(arr, left = 0, right = arr.length - 1) {
  // 定义递归边界,若数组只有一个元素,则没有排序必要
  if(arr.length > 1) {
      // lineIndex表示下一次划分左右子数组的索引位
      const lineIndex = partition(arr, left, right)
      // 如果左边子数组的长度不小于1,则递归快排这个子数组
      if(left < lineIndex-1) {
        // 左子数组以 lineIndex-1 为右边界
        quickSort(arr, left, lineIndex-1)
      }
      // 如果右边子数组的长度不小于1,则递归快排这个子数组
      if(lineIndex<right) {
        // 右子数组以 lineIndex 为左边界
        quickSort(arr, lineIndex, right)
      }
  }
  return arr
}
// 以基准值为轴心,划分左右子数组的过程
function partition(arr, left, right) {
  // 基准值默认取中间位置的元素
  let pivotValue = arr[Math.floor(left + (right-left)/2)]
  // 初始化左右指针
  let i = left
  let j = right
  // 当左右指针不越界时,循环执行以下逻辑
  while(i<=j) {
      // 左指针所指元素若小于基准值,则右移左指针
      while(arr[i] < pivotValue) {
          i++
      }
      // 右指针所指元素大于基准值,则左移右指针
      while(arr[j] > pivotValue) {
          j--
      }

      // 若i<=j,则意味着基准值左边存在较大元素或右边存在较小元素,交换两个元素确保左右两侧有序
      if(i<=j) {
          swap(arr, i, j)
          i++
          j--
      }

  }
  // 返回左指针索引作为下一次划分左右子数组的依据
  return i
}

// 快速排序中使用 swap 的地方比较多,我们提取成一个独立的函数
function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]]
}