经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 其他 » 计算机原理 » 查看文章
二叉堆原理与实现
来源:cnblogs  作者:血染河山  时间:2023/7/24 8:52:20  对本文有异议

二叉堆

二叉堆具有两个性质, 结构性和排序性.

结构性质

堆是一棵被完全填满的二叉树, 叫做完全二叉树, 除了底层以外都被填满了, 而最底层从左到右填入了.
image.png
以上图为例, 1是小顶堆, 2是大顶堆, 3是小顶堆, 4不是堆.
这种完全二叉树并不一定被填满, 为什么要被称作是完全二叉树呢? 这是因为这种二叉树可以被高效的保存在数组中. 二叉树通常有两种方式存储:

基于链表
image.png
每个节点存储3个字段

基于数组
image.png
这种方式中根节点存储在1位, 后面左节点存储在 2i位, 右子节点存储在2i+1位. 通过这种方式只要知道根节点的位置, 就可以利用上述的关系构建出整个树, 这样的存储非常的紧密, 不需要额外存储左右指针.
但是如果二叉树不是满二叉树按照这种方式存储就会存在空洞, 造成空间浪费
image.png

回到堆的话题, 因为堆满足完全二叉树的结构性, 所以通常堆都是存储在数组中.

堆序性质

在一个堆(小根堆)中, 对于每一个节点X, X的父亲节点 <= X节点
image.png
如上图右侧的完全二叉树就不是堆.

从上面定义的信息可以看出堆所提供的有序性是有限的, 只能知道堆顶是最大或最小值, 因此堆最常用的就是用作优先级队列, 优先级队列所需要的接口.

  • add
  • findMin
  • deleteMin

在添加和删除的时候都需要去维护上面提到的两个堆的性质.

实现

add

image.png

新插入的值默认插在末尾, 这会破快堆的有序性, 所以需要找到合适的位置放置新加入的值. 将插入的值逐步和parent比较, 如果小于parent 则上浮

样例实现

  1. public void add(int x) {
  2. if (size == elements.length - 1) {
  3. enlargeArray();
  4. }
  5. // 复制elements[0]的好处是for循环不需要对 index 0 特殊处理
  6. // i = 1时, 就会和elements[0] 作比较
  7. elements[0] = x;
  8. // percolate up / sift up
  9. // i 要从 ++size算起, 不然找的parent可能是错的
  10. for (int i = ++size; i >= 1; i = i / 2) {
  11. // 插入值小于父节点, 将父节点下移, 空穴上移
  12. if (x < elements[i / 2]) {
  13. elements[i] = elements[i / 2];
  14. } else {
  15. elements[i] = x;
  16. break;
  17. }
  18. }
  19. }

deleteMin

image.png
image.png
删除最小值后, 根节点就变成了空值, 为了保障堆的结构性, 将最后一位填充到根节点处, 同时这不一定满足堆序性质, 所以要和左右子树逐步比较下沉.

这里我开始思考的是另一种思路实现

  1. 判断是否右子树为空或者左子树小于右子树, 如果是则左子树上移, 否则右子树上移
  2. 退出条件为移动到没有子树的节点 即 i > size /2
  3. 退出后将最后一位赋值给当前值

需要注意的是左右 子树不一定都存在

  1. private void deleteMin() {
  2. int i = 1;
  3. if (size == 1) {
  4. size = 0;
  5. return;
  6. }
  7. for (; i <= size / 2;) {
  8. // 右子树为空或者左子树小于右子树
  9. if (2 * i + 1 > size || elements[2 * i] < elements[2 * i + 1]) {
  10. // 左子树上移
  11. elements[i] = elements[2 * i];
  12. i = 2 * i;
  13. } else {
  14. elements[i] = elements[2 * i + 1];
  15. i = 2 * i + 1;
  16. }
  17. }
  18. elements[i] = elements[size];
  19. size--;
  20. }

image.png
书上的解法

  1. 首先将last element赋值给array[1]
  2. 将array[1] 下沉

这两种实现差别

  • 第一种的思路是不断的在两个子树中查找较小值提升
  • 第二种思路是将末尾设置为root后, 不断地将这个值下沉, 第二种更符合算法的 percolate down的描述. 而且第一种一定会比较logN次, 而第二种实际上是可能会提前终止的. 所以第二种的最优运行情况中的比较次数会更少

打印

同样添加了一个方法来打印堆的结构方便检验准确性

  1. private void print() {
  2. int currentLevel = 0;
  3. int currentIndent = 0;
  4. int totalLevel = (int) Math.floor(Math.log(size) / Math.log(2)) + 1;
  5. StringBuilder levelBuilder = new StringBuilder();
  6. levelBuilder.append(String.format("level% 2d: ", currentLevel));
  7. for (int i = 1; i <= size; i++) {
  8. // 不精确的计算 log2(N)
  9. int level = (int) Math.floor(Math.log(i) / Math.log(2));
  10. if (level != currentLevel) {
  11. System.out.println(levelBuilder.toString());
  12. currentLevel = level;
  13. currentIndent = 0;
  14. levelBuilder = new StringBuilder();
  15. levelBuilder.append(String.format("level% 2d: ", currentLevel));
  16. }
  17. int toLeftIntent = indent(i, totalLevel - 1);
  18. levelBuilder.append(blank((toLeftIntent - currentIndent), currentLevel));
  19. currentIndent = toLeftIntent;
  20. levelBuilder.append(String.format("%d", elements[i]));
  21. }
  22. System.out.println(levelBuilder.toString());
  23. // for (int i = 1; i <= size; i++) {
  24. // System.out.printf("%d ", elements[i]);
  25. // }
  26. }

参考

<数据结构与算法分析 Java描述> 6.3 BinaryHeap
完全二叉树看起来并不完全,为什么叫完全二叉树呢?
https://blog.csdn.net/qq_42006733/article/details/104580717

原文链接:https://www.cnblogs.com/Aitozi/p/17576271.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号