title: 堆排序

React 算法之堆排序

概念

二叉堆是一种特殊的堆, 二叉堆是完全二叉树或者近似完全二叉树.

堆排序是利用二叉堆的特性, 对根节点(最大或最小)进行循环提取, 从而达到排序目的(堆排序本质上是一种选择排序), 时间复杂度为O(nlog n).

特性

  1. 父节点的值>=子节点的值(最大堆), 父节点的值<=子节点的值(最小堆). 每个节点的左子树和右子树都是一个二叉堆.
  2. 假设一个数组[k0, k1, k2, ...kn]下标从 0 开始. 则ki <= k2i+1,ki <= k2i+2 或者 ki >= k2i+1,ki >= k2i+2 (i = 0,1,2,3 .. n/2)

基本使用

假设现在有一个乱序数组, [5,8,0,10,4,6,1], 现在将其构造成一个最小堆

  1. 构造二叉堆
    • 需要从最后一个非叶子节点开始, 向下调整堆结构

React 算法之堆排序 - 图1

  1. 插入节点, 重新向上调整堆(sift-up)
    • 将新元素插入到数组末尾之后, 要重新调整数组结构, 保证数组任然是最小(或最大)堆.

React 算法之堆排序 - 图2

  1. 提取或删除根节点(顶端节点), 重新向下调整堆(sift-down)
    • 对于最大堆, 提取的是最大值. 对于最小堆, 提取的是最小值.
    • 顶点被提取之后, 要重新调整数组结构, 保证数组任然是最小(或最大)堆.

React 算法之堆排序 - 图3

  1. 排序过程

利用二叉堆的特性, 排序就是循环提取根节点的过程. 循环执行步骤 3, 直到将所有的节点都提取完成, 被提取的节点构成的数组就是一个有序数组.

注意:

  • 如需升序排序, 应该构造最大堆. 因为最大的元素最先被提取出来, 被放置到了数组的最后, 最终数组中最后一个元素为最大元素.
  • 如需降序排序, 应该构造最小堆. 因为最小的元素最先被提取出来, 被放置到了数组的最后, 最终数组中最后一个元素为最小元素.
  • 堆排序是一种不稳定排序(对于相同大小的元素, 在排序之后有可能和排序前的先后次序被打乱).

代码演示

将乱序数组[5,8,0,10,4,6,1]降序排列

步骤:

  1. 构造最小堆
  2. 循环提取根节点, 直到全部提取完
  1. const minHeapSort = arr => {
  2. // 1. 构造最小堆
  3. buildMinHeap(arr);
  4. // 2. 循环提取根节点arr[0], 直到全部提取完
  5. for (let i = arr.length - 1; i > 0; i--) {
  6. let tmp = arr[0];
  7. arr[0] = arr[i];
  8. arr[i] = tmp;
  9. siftDown(arr, 0, i - 1);
  10. }
  11. };
  12. // 把整个数组构造成最小堆
  13. const buildMinHeap = arr => {
  14. if(arr.length < 2) {
  15. return arr;
  16. }
  17. const startIndex = Math.floor(arr.length / 2 - 1);
  18. for (let i = startIndex; i >= 0; i--) {
  19. siftDown(arr, i, arr.length - 1);
  20. }
  21. };
  22. // 从startIndex索引开始, 向下调整最小堆
  23. const siftDown = (arr, startIndex, endIndex) => {
  24. const leftChildIndx = 2 * startIndex + 1;
  25. const rightChildIndx = 2 * startIndex + 2;
  26. let swapIndex = startIndex;
  27. let tmpNode = arr[startIndex];
  28. if (leftChildIndx <= endIndex) {
  29. if (arr[leftChildIndx] < tmpNode) {
  30. // 待定是否交换, 因为right子节点有可能更小
  31. tmpNode = arr[leftChildIndx];
  32. swapIndex = leftChildIndx;
  33. }
  34. }
  35. if (rightChildIndx <= endIndex) {
  36. if (arr[rightChildIndx] < tmpNode) {
  37. // 比left节点更小, 替换swapIndex
  38. tmpNode = arr[rightChildIndx];
  39. swapIndex = rightChildIndx;
  40. }
  41. }
  42. if (swapIndex !== startIndex) {
  43. // 1.交换节点
  44. arr[swapIndex] = arr[startIndex];
  45. arr[startIndex] = tmpNode;
  46. // 2. 递归调用, 继续向下调整
  47. siftDown(arr, swapIndex, endIndex);
  48. }
  49. };

测试:

  1. var arr1 = [5, 8, 0, 10, 4, 6, 1];
  2. minHeapSort(arr1);
  3. console.log(arr1); // [10, 8, 6, 5,4, 1, 0]
  4. var arr2 = [5];
  5. minHeapSort(arr2);
  6. console.log(arr2); // [ 5 ]
  7. var arr3 = [5, 1];
  8. minHeapSort(arr3);
  9. console.log(arr3); //[ 5, 1 ]

React 当中的使用场景

对于二叉堆的应用是在scheduler包中, 有 2 个数组taskQueuetimerQueue, 它们都是以最小堆的形式进行存储, 这样就能保证以O(1)的时间复杂度, 取到数组顶端的对象(优先级最高的 task).

具体的调用过程被封装到了SchedulerMinHeap.js, 其中有 2 个函数siftUp,siftDown分别对应向上调整和向下调整.

  1. type Heap = Array<Node>;
  2. type Node = {|
  3. id: number,
  4. sortIndex: number,
  5. |};
  6. // 添加新节点, 添加之后, 需要调用`siftUp`函数向上调整堆.
  7. export function push(heap: Heap, node: Node): void {
  8. const index = heap.length;
  9. heap.push(node);
  10. siftUp(heap, node, index);
  11. }
  12. // 查看堆的顶点, 也就是优先级最高的`task`或`timer`
  13. export function peek(heap: Heap): Node | null {
  14. const first = heap[0];
  15. return first === undefined ? null : first;
  16. }
  17. // 将堆的顶点提取出来, 并删除顶点之后, 需要调用`siftDown`函数向下调整堆.
  18. export function pop(heap: Heap): Node | null {
  19. const first = heap[0];
  20. if (first !== undefined) {
  21. const last = heap.pop();
  22. if (last !== first) {
  23. heap[0] = last;
  24. siftDown(heap, last, 0);
  25. }
  26. return first;
  27. } else {
  28. return null;
  29. }
  30. }
  31. // 当插入节点之后, 需要向上调整堆结构, 保证数组是一个最小堆.
  32. function siftUp(heap, node, i) {
  33. let index = i;
  34. while (true) {
  35. const parentIndex = (index - 1) >>> 1;
  36. const parent = heap[parentIndex];
  37. if (parent !== undefined && compare(parent, node) > 0) {
  38. // The parent is larger. Swap positions.
  39. heap[parentIndex] = node;
  40. heap[index] = parent;
  41. index = parentIndex;
  42. } else {
  43. // The parent is smaller. Exit.
  44. return;
  45. }
  46. }
  47. }
  48. // 向下调整堆结构, 保证数组是一个最小堆.
  49. function siftDown(heap, node, i) {
  50. let index = i;
  51. const length = heap.length;
  52. while (index < length) {
  53. const leftIndex = (index + 1) * 2 - 1;
  54. const left = heap[leftIndex];
  55. const rightIndex = leftIndex + 1;
  56. const right = heap[rightIndex];
  57. // If the left or right node is smaller, swap with the smaller of those.
  58. if (left !== undefined && compare(left, node) < 0) {
  59. if (right !== undefined && compare(right, left) < 0) {
  60. heap[index] = right;
  61. heap[rightIndex] = node;
  62. index = rightIndex;
  63. } else {
  64. heap[index] = left;
  65. heap[leftIndex] = node;
  66. index = leftIndex;
  67. }
  68. } else if (right !== undefined && compare(right, node) < 0) {
  69. heap[index] = right;
  70. heap[rightIndex] = node;
  71. index = rightIndex;
  72. } else {
  73. // Neither child is smaller. Exit.
  74. return;
  75. }
  76. }
  77. }
  • peek函数: 查看堆的顶点, 也就是优先级最高的tasktimer.
  • pop函数: 将堆的顶点提取出来, 并删除顶点之后, 需要调用siftDown函数向下调整堆.
  • push函数: 添加新节点, 添加之后, 需要调用siftUp函数向上调整堆.
  • siftDown函数: 向下调整堆结构, 保证数组是一个最小堆.
  • siftUp函数: 当插入节点之后, 需要向上调整堆结构, 保证数组是一个最小堆.

总结

本节介绍了堆排序的基本使用, 并说明了堆排序react源码中的应用. 在阅读scheduler包的源码时, 会更加清晰的理解作者的思路.