JavaScript
算法
【慕课】重学算法 - part.2 高级排序 (1)
10/3/2018
·
12
·
profile photo
这系列文章是慕课网《算法与数据结构》实战课程老师的讲授内容笔记整理,其中有很多动图都是我参考老师的动画演示自己制作的,并提供 JS(es6) 版的代码示例。代码仓库:📦 https://github.com/Mitscherlich/Play-with-Algorithms-JS
虽然  级的排序算法实现简单,但显然不能因为不能应用在大规模数据上而不具有实际生产价值。接下来我们将学习几种相对高级的排序算法,它们分别有不同的应用优势,并且能够非常迅速的处理数百万级别的数据量,这使得他们成为了现代计算机科学和互联网行业的支柱算法,同时也为我们接下来将要介绍的其他算法与数据结构打下了基础。

 比  快多少?

首先来感性的认识一下, 究竟比  快多少:
无图无真相
无图无真相

归并排序

归并排序是将为大家介绍的第一个高级排序算法。归并排序使用递归过程,首先将一个数组一分为二,想办法对这两部分进行排序,然后通过归并 (Merge) 操作将这两部分合并起来,最后得到一个排序的数组。简单来说,归并排序的算法流程为:
  1. 将一个数组一分为二,分别对两个子数组进行排序;
  1. 对子数组排序时,继续将子数组分为两部分,直到每部分只有不多于 1 个元素 (并不一定,接下来会有说明);
  1. 逐级向上归并,得到排序后的数组。
动画演示:
notion image
大家应该也不难看出,图示中对 8 个元素进行排序,一共分出来了 3 层,而 3 正是  的结果;事实上这也是这一类算法  级别复杂度的理论依据,在这里是通过不断二分,将排序拆分为了一个  级别的问题,再通过内层  级别的归并操作,最终得到了  的时间复杂度;广义地,如果是一个 分问题,最后的时间复杂度则是 

归并过程

从上面的流程中可以看出来,算法的关键就在于实现这个 Merge 操作。幸运的是,存在这样的操作,使得归并可以在  级别内完成。
我们先来看一下归并操作的动画演示:
notion image
归并操作的核心是在一个相同大小的辅助空间上,分别维护两个子数组 (已经排好序),将合适的元素放置到原数组合适的位置上。简单来说,记原数组当前要放置的元素索引位置为 k,两个子数组待比较的元素索引分别为 i 和 j,则归并过程的算法可以表述为:
  1. 比较 i 与 j 索引位置元素的大小,将较小(或大)的元素放置到原数组索引 k 的位置上;
  1. 重复此步骤,直到 i 或者 j 超出子数组索引范围;
  1. 若子数组仍有剩余元素,则依次放置到原数组末尾,直到子数组均遍历完成。
// 将 arr[l...mid] 和 arr[mid+1...r] 两部分进行归并
function merge (array, l, mid, r, cb) {
  const aux = new Array(r - l + 1)
  for (let i = l; i <= r; i++) {
    aux[i - l] = array[i]
  }
  // 初始化,i 指向左半部分的起始索引位置 l;j 指向右半部分起始索引位置 mid+1
  let i = l
  let j = mid + 1
  for (let k = l; k <= r; k++) {
    if (i > mid) { // 如果左半部分元素已经全部处理完毕
      array[k] = aux[j++ - l]
    } else if (j > r) { // 如果右半部分元素已经全部处理完毕
      array[k] = aux[i++ - l]
    } else if (cb(aux[i - l], aux[j - l])) { // 左半部分所指元素 < 右半部分所指元素
      array[k] = aux[i++ - l]
    } else { // 左半部分所指元素 >= 右半部分所指元素
      array[k] = aux[j++ - l]
    }
  }
}

// 递归使用归并排序, 对 arr[l...r] 的范围进行排序
function mergeSort (array, l, r, cb) {
  if (l >= r) {
    return
  }
  const mid = Math.floor((l + r) / 2)
  mergeSort(array, l, mid, cb)
  mergeSort(array, mid + 1, r, cb)
  merge(array, l, mid, r, cb)
}

Array.prototype.mergeSort = function (cb) {
  const array = this.slice()
  const n = array.length
  mergeSort(array, 0, n - 1, cb)
  return array
}
示例代码 src/03-Sorting-Advance/02-Merge-Sort.js
稍加思索便可意识到,我们其实不必将数组细分至只有一个元素,对待一个小的数组,我们完全可以使用插入排序作为子过程来加速排序的进程:
function insertSortPartial (array, l, cb) {
  for (let i = l + 1; i <= r; i++) {
    let e = array[i]
    let j
    for (j = i; j > l && !cb(array[j - 1], e); j--) {
      array[j] = array[j - 1]
    }
    array[j] = e
  }
  return array
}

// 递归使用归并排序, 对 arr[l...r] 的范围进行排序
function mergeSortEnhance (array, l, r, cb) {
  // 优化 1: 对于小规模数组, 使用插入排序
  if (r - l <= 15) {
    array = insertSortPartial(array, l, r, cb)
    return
  }
  const mid = Math.floor((l + r) / 2)
  mergeSortEnhance(array, l, mid, cb)
  mergeSortEnhance(array, mid + 1, r, cb)
  // 优化 2: 对于 arr[mid] <= arr[mid+1] 的情况, 不进行 merge
  // 对于近乎有序的数组非常有效, 但是对于一般情况, 有一定的性能损失
  if (!cb(array[mid], array[mid + 1])) {
    merge(array, l, mid, r, cb)
  }
}

Array.prototype.mergeSortEnhance = function (cb) {
  const array = this.slice()
  const n = array.length
  mergeSortEnhance(array, 0, n - 1, cb)
  return array
}
示例代码 src/03-Sorting-Advance/03-Merge-Sort-Enhance.js

自顶向下的归并排序

我们已经使用递归这种方法完成了归并操作的实现。然而在计算机上,递归操作往往需要软件保存递归过程中的堆栈信息,这在递归层数较深是会造成一定的性能浪费,我们也很容易就能写出一种不需要递归的、自顶向下的归并排序算法:
Array.prototype.mergeSortReverse = function (cb) {
  const array = this.slice()
  const n = array.length
  for (let sz = 1; sz <= n; sz += sz) {
    for (let i = 0; i + sz < n; i += sz + sz) {
      // 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并
      merge(array, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1), cb)
    }
  }
  return array
}

// Merge Sort Bottom Up 优化
Array.prototype.mergeSortReverseEnhance = function (cb) {
  const array = this.slice()
  const n = array.length
  // 对于小数组, 使用插入排序优化
  for (let i = 0; i < n; i += 16) {
    insertSortPartial(array, i, Math.min(i + 15, n - 1), cb)
  }

  for (let sz = 16; sz < n; sz += sz) {
    for (let i = 0; i + sz < n; i += sz + sz) {
      // 对于 arr[mid] <= arr[mid+1] 的情况, 不进行 merge
      if (!cb(array[i + sz - 1], array[i + sz])) {
        merge(array, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1), cb)
      }
    }
  }
  return array
}
示例代码 src/03-Sorting-Advance/04-Merge-Sort-Bottom-Up.js

小结

这一节我们用了较大的篇幅来讨论归并排序,不仅仅是因为他是我们学习的第一个  复杂度的高级排序算法,更因为它蕴含的排序思想,并且归并排序的性能也相当优越,在实际的测试中可以在 1s 中内排序 100万不同类型的数据,这也在后面的讲解中作为重要的性能指标的参考依据。