【数据结构与算法拓展】二叉堆原理、实现与例题(C和java)
前言
数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。
也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。
此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。
欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。
二叉堆原理
这一节,我们我们来介绍一个非常有趣且经典的数据结构——二叉堆
二叉堆的主要作用场合在于维护一个集合,使其能够以最快的速度获取集合中的最值,同时支持弹出最值、插入新的值,并以尽可能小的代价维护它。
具体一点说,是希望每次可以以O(1)O(1)O(1)的复杂度获取最值。同时删除最值和插入新值后应该在O(log2n)O(log_2n)O(log2n)的时间内完成对集合的维护。那么根据获取的最值是极大值还是极小值可以将堆分为大根堆和小根堆。
这个数据结构之所以经典,是因为他在很多经典的算法中都有着应用的身影:哈夫曼编码算法,dijkstra算法,堆排序算法等等不胜枚举。这正是因为它解决的问题是最基础,最普遍的一类问题——求集合最值。
那么如何维护一个集合使其能够快速的获得最值呢。其实这个问题可以借鉴生活中的经验:沙堆
诸位来看这张图片
有没有一些灵感迸发呢?
还没有也没关系,我们来接着看这张二叉树的图片
可以发现,如果将集合中的数据填在每个结点上,并且保证层级间的大小关系,那么根据树的递归定义性质就能够轻松的将全部的数据构造成一颗二叉树并且在二叉树的根部取得最值。据说灵感来自于淘汰赛制。
以小根堆为例,对于集合{1,6,8,10,2,5,15}\{1,6,8,10,2,5,15\}{1,6,8,10,2,5,15},可以构造成如下的二叉树:
对于上图树中的每个结点来说,其值都小于其子节点的值。
有的朋友会说,这样一来,二叉树形态岂不是可以乱来?毕竟满足这个条件的二叉树情况太多了。
我说小伙子你不懂,构建堆要将武德,可不能乱来。如果你乱建堆导致算法出错,那希望你耗子尾汁。
二叉堆存储
二叉堆中结点的位置和顺序有讲究,首先需要它是一棵完全二叉树,其中结点的位置又和存储有很大关系。毕竟:
只讨论逻辑而不考虑存储的数据结构是有灵魂而无肉体的
——沃茨基硕德
如何存储这个二叉树才好呢?我们要先了解一下完全二叉树的概念,下面不妨截取一段来自百度百科的定义
可以看到,完全二叉树中每个位置的结点都是有确定且紧密排列的编号的,这就能够使我们想到前面学到的数组。实际上,用数组完全可以存储完全二叉树,我们也经常要这么做
所以在存储堆的问题上,就采用顺序存储结构来完成对二叉树的存储。
按照上面的定义,给二叉堆的结点按照从上到下,从左到右的顺序编个号,就像下面这样:
将上面的二叉树存储在数组中就是:
于是根据这些规则就会产生一些不得了的性质:
- 根节点的编号为1
- 结点k的父节点为⌊k2⌋\lfloor \frac k 2 \rfloor⌊2k⌋
- 结点k的左儿子为k∗2k * 2k∗2
- 结点k的右儿子为k∗2+1k*2 + 1k∗2+1
有了这些性质,一个堆便被我们表示成为一个数组了。
二叉堆维护及实现
让我们先用数组封装二叉堆的底层,接着再来一步步的完成二叉堆的各项需求,为了演示方便,我们还是以小根堆为例:
//c
#define N ...
int heap[N];//这个数组用来存放二叉树结点。
int size = 0;//堆中当前结点数
//java
public class Heap {private int[] heap;private int size;private int maxSize;public Heap(int maxSize) {this.maxSize = maxSize;heap = new int[maxSize];}
}
二叉堆维护
接下来我们要介绍维护二叉堆的两个至关重要的操作:上浮(up)和下沉(down)
他们是二叉堆保持性质的基础
上浮操作意在将一个元素不断地向上和父节点交换,直到该元素比父元素大或成为堆顶
下沉操作意在将一个元素不断地向下和子节点交换,直到该元素比子元素小或无子元素
上浮和下沉操作是堆结构的重中之重,建议读者重点理解。
上浮操作:
对于上浮操作,我们给出如下算法:
- 如果该节点为堆顶,退出循环
- 如果该节点大于等于父节点,退出循环
- 交换父节点与该节点,重复上述过程
举例来说:
代码实现如下,其中的乘2和整除2可以用位运算>>
和<<
来代替:
//C
void up(int k){int fa = k >> 1;while(fa != 0){if(heap[fa] <= heap[k]){//如果父节点小于当前结点则不需要继续上浮break;}//交换父节点和当前结点int temp = heap[fa];heap[fa] = heap[k];heap[k] = temp;//指向父节点重复过程k = fa;fa >>= 1; }
}
//java
private void up(int k) {int fa = k >> 1;while(fa >= 1) {if(heap[k] <= heap[fa]) {break;}int temp = heap[k];heap[k] = heap[fa];heap[fa] = temp;k = fa;fa >>= 1;}
}
下沉操作
下沉操作与上浮操作类似,不过在选择交换的子节点时保证选择到的子节点要是最小的,不然在交换后不能保证父节点比子节点都小。
具体步骤如下:
- 如果节点无子节点,则停止下沉
- 选择子节点中较小者
- 如果该节点小于等于较小子节点,则结束下沉过程
- 交换该节点和子节点值,重复上述过程
代码实现如下:
//C
void down(int k){int son = k << 1;while(son <= size){if(son + 1 <= size && heap[son + 1] < heap[son]){//son为左节点,son+1为右节点,比较选最小son++;}if(heap[k] <= heap[son]){//如果最小的儿子没有父亲小,则停止下沉break;}//交换int temp = heap[k];heap[k] = heap[son];heap[son] = temp;//指针指向子节点,继续该过程k = son;son <<= 1;}
}
//java
private void down(int k) {int son = k << 1;while(son <= size) {if(son + 1 <= size && heap[son + 1] < heap[son]) {son++;}if(heap[k] <= heap[son]) {break;}int temp = heap[k];heap[k] = heap[son];heap[son] = temp;k = son;son <<= 1;}
}
插入结点
哦吼,有了上面两个至关重要的操作的铺垫,下面我们就可以来实现堆的基础方法了。首先就是能向其中插入一个节点。
思路很简单,就是:
- 将新节点插入到末尾
- 对新节点进行上浮
前文的上浮操作模拟的就是这一过程
代码实现如下:
//C
void add(int k){heap[++size];up(size);
}
//java
public void add(int k) {heap[++size] = k;up(size);
}
看吧,如此简单~
获取最值
在给出二叉堆定义时,我们说过最值存在于堆顶,而堆顶就是数组中的首位元素,所以获取堆顶就是直接获取数组中的首位元素:
//C
int peak(){if(size == 0){return 0;}return heap[1];
}
//java
public int peak() {if(size > 0) {return heap[0];}return 0;
}
弹出堆顶
弹出堆顶操作需要删除堆顶元素,并重新调整堆的形态,因为删除堆顶会使堆得规模-1,所以可以使用如下步骤
- 使用最后一个元素覆盖堆顶
- 对新的堆顶进行下沉操作
上文中的下沉操作示例就在模拟这个过程
代码实现如下:
//C
void pop(){if(size == 0){return;}head[1] = head[size--];down(1);
}
//java
public void pop() {if(size == 0){return;}heap[1] = heap[size--];down(1);
}
所以总的来说,二叉堆的功能实现是基于上浮和下沉操作的。所以还是墙裂 强烈建议读者把他们先搞明白
完整代码
//C
#define N ...
int heap[N];//这个数组用来存放二叉树结点。
int size = 0;//堆中当前结点数void up(int k){//上浮操作int fa = k >> 1;while(fa != 0){if(heap[fa] <= heap[k]){//如果父节点小于当前结点则不需要继续上浮break;}//交换父节点和当前结点int temp = heap[fa];heap[fa] = heap[k];heap[k] = temp;//指向父节点重复过程k = fa;fa >>= 1; }
}void down(int k){//下沉操作int son = k << 1;while(son <= size){if(son + 1 <= size && heap[son + 1] < heap[son]){//son为左节点,son+1为右节点,比较选最小son++;}if(heap[k] <= heap[son]){//如果最小的儿子没有父亲小,则停止下沉break;}//交换int temp = heap[k];heap[k] = heap[son];heap[son] = temp;//指针指向子节点,继续该过程k = son;son <<= 1;}
}void add(int k){//添加结点heap[++size];up(size);
}int peak(){//获取最值if(size == 0){return 0;}return heap[1];
}void pop(){//弹出结点if(size == 0){return;}head[1] = head[size--];down(1);
}
//java
public class Heap {private int[] heap;private int size;private int maxSize;public Heap(int maxSize) {this.maxSize = maxSize;heap = new int[maxSize];}public int peak() {if(size > 0) {return heap[0];}return 0;}private void up(int k) {int fa = k >> 1;while(fa >= 1) {if(heap[k] <= heap[fa]) {break;}int temp = heap[k];heap[k] = heap[fa];heap[fa] = temp;k = fa;fa >>= 1;}}private void down(int k) {int son = k << 1;while(son <= size) {if(son + 1 <= size && heap[son + 1] < heap[son]) {son++;}if(heap[k] <= heap[son]) {break;}int temp = heap[k];heap[k] = heap[son];heap[son] = temp;k = son;son <<= 1;}}public void pop() {if(size == 0) {return;}heap[1] = heap[size--];down(1);}public void add(int k) {heap[++size] = k;up(size);}
}
二叉堆例题
堆的经典例题有很多,一道绕不开的题目就是合并果子
题目大意是给定n堆果子,每次可以选择合并任意两堆,代价为两堆果子的总和。问将所有果子合并称为一堆的最小代价是多少
思路就是每次寻找当前队列中最少的两堆果子,合并成为一堆。
这个过程如果使用堆来解决就是如下步骤:
- 将n堆果子入堆
- 每次拿出最小的两堆(取最小,弹堆顶,再取最小,再弹堆顶)
- 合并成为一堆,记录答案
- 将新的一堆加入堆中
- 重复上述过程直至剩下一堆果子
代码实现如下:
//C
#define N 100050
int heap[N];//这个数组用来存放二叉树结点。
int size = 0;//堆中当前结点数void up(int k){//上浮操作int fa = k >> 1;while(fa != 0){if(heap[fa] <= heap[k]){//如果父节点小于当前结点则不需要继续上浮break;}//交换父节点和当前结点int temp = heap[fa];heap[fa] = heap[k];heap[k] = temp;//指向父节点重复过程k = fa;fa >>= 1; }
}void down(int k){//下沉操作int son = k << 1;while(son <= size){if(son + 1 <= size && heap[son + 1] < heap[son]){//son为左节点,son+1为右节点,比较选最小son++;}if(heap[k] <= heap[son]){//如果最小的儿子没有父亲小,则停止下沉break;}//交换int temp = heap[k];heap[k] = heap[son];heap[son] = temp;//指针指向子节点,继续该过程k = son;son <<= 1;}
}void add(int k){//添加结点heap[++size];up(size);
}int peak(){//获取最值if(size == 0){return 0;}return heap[1];
}void pop(){//弹出结点if(size == 0){return;}head[1] = head[size--];down(1);
}int main(){int n;scanf("%d",&n);int k;for(int i = 0;i < n;i++){scanf("%d",&k);add(k);}int ans = 0;while(size > 1){k = peak();pop();k += peak();pop();ans += k;add(k);}printf("%d",ans);
}
二叉堆与优先队列
堆很好用,但是他更偏向底层。现在在讨论相关数据结构更多的用的是优先队列的概念。
优先队列就是一种纯粹的概念,定义了一个队列,这个队列可以满足获取最大值,弹出最大值,插入结点这三个操作。
其实如果只关注这三个需求,有很多方式可以实现他们,包括暴力的插入或查询。或者使用平衡树等等。
所以从这个层面来看,优先队列的范围更广一些,二叉堆可看作是优先队列的一种实现方式,这就像我们说线性表可以使用顺序表(数组)和链表来实现。不过由于二叉堆相较于平衡树更好实现所以一般情况下优先队列和二叉堆是可以划等号的。
因此各位同志下次再看到这两个名词暧昧不清的时候,就不会再一头雾水啦~
往期博客
- 【数据结构基础】数据结构基础概念
- 【数据结构基础】线性数据结构——线性表概念 及 数组的封装
- 【数据结构基础】线性数据结构——三种链表的总结及封装
- 【数据结构基础】线性数据结构——栈和队列的总结及封装(C和java)
- 【算法与数据结构基础】模式匹配问题与KMP算法
- 【数据结构与算法基础】二叉树与其遍历序列的互化 附代码实现(C和java)
- 【数据结构与算法拓展】 单调队列原理及代码实现
- 【数据结构基础】图的存储结构
- 【数据结构与算法基础】并查集原理、封装实现及例题解析(C和java)
参考资料:
- 《数据结构》(刘大有,杨博等编著)
- 《算法导论》(托马斯·科尔曼等编著)
- 《图解数据结构——使用Java》(胡昭民著)
- OI WiKi
【数据结构与算法拓展】二叉堆原理、实现与例题(C和java)相关推荐
- 【数据结构与算法】二叉堆V2.0的Java实现
更新说明 我们在此前已经编写过简单版的二叉大根堆V1.0,这次,换成二叉小根堆,命名为二叉堆V2.0. 大家也知道,堆是完全二叉树,存储方式借助动态数组实现顺序存储,依赖于父子结点之间的index关系 ...
- 【数据结构与算法】二叉堆与二叉搜索树的区别
问题描述 记得刚学习数据结构的时候,就容易混淆二叉堆和二叉搜索树,其实虽说堆也是一种完全二叉树,但二者差别还是挺大的,本文试做分析. 逻辑结构 二叉堆和二叉搜索树都是结点带权重,并在父子结点间满足某种 ...
- 二叉堆 - 原理与实现
1. 概要 本章介绍二叉堆,二叉堆就是通常我们所说的数据结构中"堆"中的一种.和以往一样,本文会先对二叉堆的理论知识进行简单介绍,然后给出C语言的实现.后续再分别给出C++和Jav ...
- 《数据结构与算法之二叉平衡树(AVL)》
说在前头:本人为大二在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正.若在阅 ...
- 漫画算法笔记 二叉堆基本操作
漫画算法笔记 二叉堆基本操作 #include <iostream> #include <stdlib.h> #include <vector> using nam ...
- 《恋上数据结构第1季》二叉堆原理及实现、最小堆解决 TOP K 问题
二叉堆 BinaryHeap 堆(Heap) 堆的出现 堆简介 二叉堆(Binary Heap) 获取最大值 最大堆 - 添加 最大堆 - 添加优化 最大堆 - 删除 replace 最大堆 - 批量 ...
- 数据结构:二叉堆原理及实现
二叉堆 二叉堆数组 最小堆和最大堆 最小堆的实现 创建最小堆及初始化参数 二叉堆数组扩容 数组索引值对应的value交换 上浮 下沉 添加value 获取堆顶最值 任意索引的数据上浮或下沉 二叉堆数组 ...
- 最短路径——Dijkstra算法以及二叉堆优化(含证明)
一般最短路径算法习惯性的分为两种:单源最短路径算法和全顶点之间最短路径.前者是计算出从一个点出发,到达所有其余可到达顶点的距离.后者是计算出图中所有点之间的路径距离. 单源最短路径 Dijkstra算 ...
- 《数据结构与算法之二叉搜索树(Java实现)》
说在前头:本人为大二在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,能力有限,文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正.若 ...
最新文章
- utf8编码中文还是乱码_zend studio 乱码
- JPA基础(二)(转)
- 开发日记-20190910 Makefile相关(一)
- 人月神话阅读笔记02
- 【Android 逆向】Android 中常用的 so 动态库 ( 拷贝 /system/lib/ 中的 Android 系统 so 动态库 )
- 修改CodeSmith中的SchemaExplorer.MySQLSchemaProvider
- struts2.3.12+hibernate4.3.11+spring4.2.2整合问题2java.lang.ClassNotFoundException: org.springframework.w
- OpenCV调用TensorFlow预训练模型
- SpringSecurity使用自定义认证页面
- Android 如何添加一种锁屏方式
- 【矩阵乘法】生成树计数(luogu 2109/NOI 2007)
- uos配置 java 环境变量_CentOS 7.3 环境配置java和tomcat开机启动
- where和having区别
- python入门第八章 商品数量检测 头像格式检测
- Exchange 日常管理之二:设置邮件转发
- 【SVM预测】基于蝙蝠算法改进SVM实现数据分类
- 支付宝、财付通、网银、百度钱包、京东钱包接口费率
- 稻盛和夫《调动员工积极性的七个关键》读书笔记
- java精选视频资源,收藏慢慢看!
- Alien Skin Exposure X4 Bundle 4.5.3.66 特别版 Mac 模拟胶片效果调色滤镜
热门文章
- 解决mount时发生错误wrong fs type, bad option, bad superblock
- SSR 应用与原 CSR 应用变更同步问题实践
- 让体验更具价值 Aruba推出首个真正支持802.11ax标准的物联网无线AP
- ECLIPSE输入输出
- mysql 理论知识
- ReentrantLock加锁(lock())、释放锁(unlock())的实现
- Thinkphp5上传中文名的文件报错move_uploaded_file(): failed to open stream: Invalid argument
- 干货分享:教你快速拍照植物识别
- 详解信道估计的发展与最新研究进展(MIMO)
- 固体或粘性液体,取决于分子量,FMOC-PEG-COOH,芴甲氧羰基-聚乙二醇-羧基,acid-PEG-FMOC,取用一定要干燥,避免频繁的溶解和冻干