今天一起学习一下堆和优先队列,重点是堆排序的理解和优先队列的用法。
通过本文你将了解到以下内容:
数据结构中的堆区别于内存分配的堆,我们说的用于排序的堆是一种表示元素集合的结构,堆是一种二叉树。
看看维基百科中对于堆的定义和说明:
堆是计算机科学中的一种特别的树状数据结构。
堆始于J. W. J. Williams在1964年发表的堆排序,当时他提出了二叉堆树作为此算法的数据结构,堆在戴克斯特拉算法和带优先级队列中亦为重要的关键。
若是满足以下特性,即可称为堆:给定堆中任意节点P和C,若P是C的母节点,那么P的值会小于等于C的值。若母节点的值恒小于等于子节点的值,此堆称为最小堆;反之称为最大堆。
堆有两个决定性特性:元素顺序和树的形状
在堆中任何结点与其子结点的大小都遵守数值大小关系。
A. 如果结点大于等于其所有子结点,也就是堆的根是所有元素中最大的,这种堆称为大根堆(大顶堆、最大堆);
B. 如果结点小于等于其所有子结点,也就是堆的根是所有元素中最小的,这种堆称为小根堆(小顶堆、最小堆);
C. 大根堆/小根堆只是约定了父结点和子结点的大小关系,但是并不约束子结点的相对大小和顺序;
如图为小根堆结构:
堆这种二叉树最多在两层具有叶子结点,并且最底层的叶子结点靠左分布,该树种不存在空闲位置,也就是堆是个完全二叉树。
上述的两种性质可以保证快捷找到最值,并且在插入和删除新元素时可以实现重新组织再次满足堆的性质。
堆中没有空闲位置并且数组是连续的,但是数组的下标是从0开始,为了统一,我们统一从1开始,也就是root结点的数组index=1,那么可以通过数组的index可以通过父结点找到左右子结点,也可以通过子结点找到父结点。
数组的元素遍历就是堆的层次遍历的结果,因此数组存储的堆具备以下性质:
//数组下标范围
i<=n && i>=1
//根结点下标为1
root_index = 1
//层次遍历第i个结点的值等于数组第i个元素
value(i) = array[i]
//堆中第i个元素的左孩子下标i*2
left_child_index(i) = i*2
//堆中第i个元素的右孩子下标i*2+1
right_child_index(i) = i*2+1
//堆中第i个元素的父结点下标i/2
parent(i) = i/2
堆和数组的对应关系如图:
堆调整的过程非常像数学归纳法的递推过程,看一下就知道,敲黑板!以下两个函数对于掌握堆非常重要。
以小根堆为例,之前a[1...n-1]满足堆的特性,在数组a[n]插入新元素之后,就产生了两种情况: A. 如果a[n]大于父结点那么a[1...n]仍然满足堆的特性,不需要调整; B. 如果a[n]比它的父结点要小无法保证堆的特性,就需要进行调整;
循环过程:自底向上的调整过程就是新加入元素不断向上比较置换的过程,直到新结点的值大于其父结点,或者新结点成为根结点为止。 停止条件:siftup是一个不断向上循环比较置换的过程,理解循环的关键是循环停止的条件,从伪码中可以清晰地看到,siftup的伪码:
//siftup运行的前置条件
heap(1,n-1) == True
void siftup(n)
i = n
loop:
// 循环停止条件一
// 已经是根结点
if i == 1:
break;
p = i/2
// 循环停止条件二
// 调整结点大于等于在此位置的父结点
if a[p] <= a[i]
break;
swap(a[p],a[i])
// 继续向上循环
i = p
siftup调整过程演示
在尾部插入元素16的调整过程如图:
以小根堆为例,之前a[1...n]满足堆的特性,在数组a[1]更新元素之后,就产生了两种情况: A. 如果a[1]小于等于子结点仍然满足堆的特性,不需要调整; B. 如果a[1]大于子结点无法保证堆的特性,就需要进行调整;
循环过程:自顶向下的调整过程就是新加入元素不断向下比较置换的过程,直到新结点的值小于等于其子结点,或者新结点成为叶结点为止。 停止条件:siftdn是一个不断向下循环比较置换的过程,理解循环的关键是循环停止的条件,从伪码中可以清晰地看到siftdn的伪码:
heap(2,n) == True
void siftdn(n)
i = 1
loop:
// 获取理论上的左孩子下标
c = 2*i
// 如果左孩子下标已经越界
// 说明当前已经是叶子结点
if c > n:
break;
//如果存在右孩子
// 则获取左右孩子中更小的一个
// 和父结点比较
if c+1 <= n:
if a[c] > a[c+1]
c++
// 父结点小于等于左右孩子结点则停止
if a[i] <= a[c]
break;
// 父结点比左右孩子结点大
// 则与其中较小的孩子结点交换
// 也就是让原来的孩子结点成为父结点
swap(a[i],a[c])
// 继续向下循环
i = c
siftdn调整过程演示
在头部元素更新为21的调整过程如图:
场景:假如有200w数据,要找最大的前10个数。
分析:那么就需要先建立大小为10个元素的小顶堆,然后再逐渐把其他所有元素依次渗透进来比较或入堆淘汰老数据或跳过,直至所有数据渗透完成,最后小根堆的10个元素就是最大的10个数了。
小根堆:选择最大的TopN个数据使用小根堆,因为堆顶就是最小的数据,每次进来的新数据只需要和堆顶比较即可,如果小于堆顶则跳过,如果大于堆顶则替换掉堆顶进行siftdn调整,来找到新进元素的正确位置,以及产生新的堆顶。
建堆过程:可以自顶向下自底向上均可,以下采用自底向上思路分析。可以将数组的叶子节点,是单个结点满足二叉堆的定义,于是从底层叶子结点的父结点从左到右,逐个向上构建二叉堆,直到第一个节点时整个数组就是一个二叉堆,这个过程是siftup和siftdn的混合,宏观上来看是自底向上,微观上每个父结点是自顶向下。
渗透排序过程:完成堆化之后,开处理N之后的元素,从N+1~200w,遇到比当前堆顶大的则与堆顶元素交换,进入堆触发siftdn调整,直至生产新的小根堆。
代码实战:leetCode 第215题 数组中的第K个最大元素。
这道题可以用堆排序来完成,建立小根堆取堆顶元素即可,验证通过的代码举例:
//leetcode 215th the Kth Num
//Source Code:C++
class Solution {
public:
//调整以当前节点为根的子树为小顶堆
int heapadjust(vector<int> &nums,int curindex,int len){
int curvalue = nums[curindex];
int child = curindex*2+1;
while(child<len){
//左右孩子中较小的那个
if(child+1<len && nums[child] > nums[child+1]){
child++;
}
//当前父节点比左右孩子其中一个大
if(curvalue > nums[child]){
nums[curindex]=nums[child];
curindex = child;
child = curindex*2+1;
}else{
break;
}
}
nums[curindex]=curvalue;
return 0;
}
int findKthLargest(vector<int>& nums, int k) {
//边界条件
if(nums.size()<k)
return -1;
//建立元素只有K个的小顶堆
//截取数组的前k个元素
vector<int> subnums(nums.begin(),nums.begin()+k);
int len = nums.size();
int sublen = subnums.size();
//将数组的前k个元素建立小顶堆
for(int i=sublen/2-1;i>=0;i--){
heapadjust(subnums,i,sublen);
}
//建立好小顶堆之后 开始逐渐吸收剩余的数组元素
//动态与堆顶元素比较 替换
for(int j=k;j<len;j++){
if(nums[j]<=subnums[0])
continue;
subnums[0] = nums[j];
heapadjust(subnums,0,sublen);
}
return subnums[0];
}
};
从实践来看,堆的重要用途是堆排序,堆排序重点是初始化堆和调整堆两个过程,然而这两个过程都离不开siftup和siftdn两个函数,因此掌握这两个函数,基本上就掌握了堆。
由于堆是二叉树,因此在实际使用中需要结合树的遍历和循环来实现堆调整,掌握堆调整过程和二叉树遍历过程,拿下堆,指日可待。
先来看看维基百科对优先队列的定义:
优先队列是计算机科学中的一类抽象数据类型。
优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。
优先队列至少需要支持下述操作:
a.插入带优先级的元素
b.取出具有最高优先级的元素
c.查看最高优先级的元素。
综合考虑插入和删除的性能 优先队列一般采用堆来实现。
由于优先队列的元素既可以是基础类型,也可以是复合类型,因此在C++中一般使用模板来定义优先队列,从而提高其可扩展性,简单定义:
template<class T>
class priqueue {
public:
//构造函数 初始化
priqueue(int maxsize);
//将T类型新元素添加到队列中
void insert(T t);
//获取队列中最小的元素
T extractmin();
};
实现优先队列的候选数据结构包括:有序序列、无序序列、堆。
综上可知,堆结构在实现优先队列的插入和删除操作时复杂性都很稳定,在大量数据的场景下优于有序序列和无序序列,因此权衡之下选择堆作为优先队列的底层数据结构。
考虑优先队列的灵活性和效率,可以基于模板化和堆来实现优先队列:
template<class T>
class priqueue {
private:
int n,maxsize;
T *x;
void swap(int i,int j)
{ T t = x[i]; x[i] = x[j]; x[j] = t; }
public:
//初始化
priqueue(int m)
{
maxsize = m;
x = new T[maxsize+1];
n = 0;
}
//插入新数据
void insert(T t)
{
int i,p;
x[++n] = t;
//末尾添加新数据 执行siftup操作
for (i = n; i > 1 && x[p=i/2] > x[i]; i = p)
swap(p,i);
}
//提取操作
T extractmin()
{
int i,c;
//提取堆顶元素
T t = x[1];
//将尾部元素放到堆顶
x[1] = x[n--];
//针对新堆顶进行siftdn操作
for (i = 1; (c = 2*i) <= n; i = c) {
if (c+1 <= n && x[c+1] < x[c])
c++;
if (x[i] <= x[c])
break;
swap(c,i);
}
return t;
}
};
上述代码摘自编程珠玑并不是STL关于优先队列的实现,只是一部分简洁的代码,旨在表现insert和extract操作时如何运用堆的siftup和siftdn操作来封装成优先队列类的基础成员函数。
3.3 优先队列的自定义优先级
模板化的优先队列扩展了使用场景,但是也产生了新的问题,就是默认的优先级比较函数不一定满足所有要求,因此很多时候都需要自己来定义优先级判定函数。
实现了一个模板优先队列需要三个参数:
#include<quque>
// 队列和优先队列的声明
std::queue<T> pq;
std::priority_queue<T, std::vector<T>, cmp> pq;
//模板化优先队列的主要参数
priority_queue<Type, Container, Func>
//举例
priority_queue<pair<char,int>,vector<pair<char,int>>,compare> pq;
//自定义比较函数
struct compare
{
bool operator()(const pair<char,int> a,const pair<char,int> b){
return b.second > a.second;
}
};
3.4 优先队列的应用
可以认为优先队列是对堆的工具化封装,加上模板和自定义比较函数两个利器加持,优先队列让使用者不再苦于堆排序的原始造轮子。
TopN问题和优先队列
仍以LeetCode 215题为例,获取数组第K大元素。
上一节中我们直接使用堆,但是构建的是小顶堆,并且大小是K个元素,遍历完成之后直接取堆顶即可,但是在数据量不大或者内存足够的情况下,可以直接建立优先队列。
默认的优先队列本质上是大顶堆,那么堆顶就不是第K大元素了,但是从堆顶开始依次pop出K-1个元素,堆顶也就是第K大元素了。
当然也可以修改比较函数实现小顶堆,取堆顶元素也是可以的,要灵活运用。使用优先队列实现LeetCode 第215题,代码如下:
//默认的比较函数是less 也就是优先队列相当于最大堆
//堆顶元素为最大值
priority_queue<int,vector<int>,less<int>> q;
int findKthLargest(vector<int>& nums, int k)
{
priority_queue<int> q(nums.begin(),nums.end());
for(int i=0;i<k-1;i++)
q.pop();
return q.top();
}
优先队列是贪心算法的重要组成部分,借助于优先队列贪心算法可以解决非常多的实际问题包括:
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/FSp95Ot3SIaCpJyVtE1P_A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。