二叉堆

二叉堆本质上是一棵完全二叉树,完全二叉树的叶子节点都排列在树的左边,体现出一种顺序的关系。
正是因为有了连续性,我们可以用数组来表示一个二叉堆。
二叉堆实现的大顶堆有一个非常重要的特性:堆中某个节点的值不会大于其父节点。
反之:小顶堆堆中某个节点的值不会小于其父节点。

二叉堆的表示

二叉堆 - 图1

数组索引从 1 开始

二叉堆 - 图2
堆是用数组来表示的,根据图中的公式,父亲节点很容易找到子节点,子节点也很容易找到父亲节点。

数组索引从 0 开始

二叉堆 - 图3

大顶堆代码实现(基础架构)

  1. /**
  2. * 最大堆实现
  3. *
  4. * @param <E> 泛型,支持多种对象
  5. */
  6. public class MaxHeap<E extends Comparable<E>> {
  7. private Array<E> data;
  8. public MaxHeap(int capacity) {
  9. this.data = new Array<>(capacity);
  10. }
  11. public MaxHeap() {
  12. this.data = new Array<>();
  13. }
  14. public int size() {
  15. return data.getSize();
  16. }
  17. public boolean isEmpty() {
  18. return data.isEmpty();
  19. }
  20. /**
  21. * 数组索引从 0 开始
  22. *
  23. * @param index 当前索引
  24. * @return 父亲节点索引
  25. */
  26. private int parent(int index) {
  27. if (index == 0) {
  28. throw new IllegalArgumentException("index-0 doesn't have parent.");
  29. }
  30. return (index - 1) / 2;
  31. }
  32. /**
  33. * @param index 当前索引
  34. * @return 左孩子的索引
  35. */
  36. private int leftChild(int index) {
  37. return index * 2 + 1;
  38. }
  39. /**
  40. * @param index 当前索引
  41. * @return 右孩子的索引
  42. */
  43. private int rightChild(int index) {
  44. return index * 2 + 2;
  45. }
  46. }

向堆中插入元素

因为二叉堆内部实现依赖于数组,所以新插入一个元素,肯定在数组的末尾的位置。
当元素插入到数组的末尾位置,假设这个元素的值很大,不符合二叉堆的性质(父亲节点大于子节点),所以这个新插入的节点需要上浮操作,调整到合适的位置,以满足二叉堆的特征。
上浮流程相对简单,每次与父亲节点比较,父亲节点的值比它小,就交换元素,继续上浮,直到父亲节点比它大或者它已经调整到了根节点。
下面用图片演示一遍:
二叉堆 - 图4
二叉堆 - 图5
二叉堆 - 图6
二叉堆 - 图7
二叉堆 - 图8

插入元素代码实现

  1. /**
  2. * 增加元素
  3. *
  4. * @param e e
  5. */
  6. public void add(E e) {
  7. data.addLast(e);
  8. siftUp(data.getSize() - 1);
  9. }
  10. /**
  11. * 上浮操作
  12. *
  13. * @param index 当前索引
  14. */
  15. private void siftUp(int index) {
  16. // index == 0,说明已经到了根节点了
  17. while (index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
  18. data.swap(index, parent(index));
  19. // 更新当前索引,继续上浮
  20. index = parent(index);
  21. }
  22. }

取出堆中最大元素

取出堆中最大元素,其实就是取出数组索引为 0 对应的值,这是由二叉堆的特性决定的,根节点元素大于所有的子节点元素。
取走了堆顶元素,我们必须指定一个新的节点作为新的堆顶节点。一般的做法是把数组的最后一位元素放在堆顶,然后将这个元素进行下沉操作,调整到合适的位置。
上面就是取出堆中最大元素的操作流程,总体上是比较清晰的。但是在实现上,我们要把父节点和左右两个子节点都进行比较,且右孩子节点不能越界。

  1. /**
  2. * @return 堆顶元素
  3. */
  4. public E findMax() {
  5. if (data.getSize() == 0) {
  6. throw new IllegalArgumentException("Can not find Max when heap is empty");
  7. }
  8. return data.get(0);
  9. }
  10. /**
  11. * 返回堆顶元素,并维护二叉堆的特性
  12. *
  13. * @return 返回堆顶元素
  14. */
  15. private E extractMax() {
  16. final E max = findMax();
  17. // 第一个元素和最后一个元素互换位置
  18. data.swap(0, data.getSize() - 1);
  19. // 删除最后一个元素
  20. data.removeLast();
  21. // 下沉操作,从堆顶开始
  22. siftDown(0);
  23. return max;
  24. }
  25. /**
  26. * 下沉操作
  27. *
  28. * @param k 索引
  29. */
  30. private void siftDown(int k) {
  31. while (leftChild(k) < data.getSize()) {
  32. int j = leftChild(k);
  33. // 比较左右孩子,谁大谁小
  34. if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
  35. j++;
  36. }
  37. // 比较父节点与子节点,父节点大于最大的子节点,满足二叉堆的特性,退出循环
  38. if (data.get(k).compareTo(data.get(j)) >= 0) {
  39. break;
  40. }
  41. data.swap(k, j);
  42. k = j;
  43. }
  44. }