深入理解堆与优先队列

这篇具有很好参考价值的文章主要介绍了深入理解堆与优先队列。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、什么是堆?

(Heap)是一种特殊的完全二叉树,满足性质:除叶节点外每个节点的值都大于等于(或者小于等于)其孩子节点的值(该性质又称「堆序性」)。

堆有两种类型:

  • 大根堆(又称最大堆):堆中每一个节点的值都大于等于其孩子节点的值。所以大根堆的特点是堆顶元素(根节点)是堆中的最大值
  • 小根堆(又称最小堆):堆中每一个节点的值都小于等于其孩子节点的值。所以小根堆的特点是堆顶元素(根节点)是堆中的最小值

下图展示了大根堆与小根堆的区别:

深入理解堆与优先队列,数据结构与算法,# AcWing,数据结构,算法,c++,堆,优先队列

二、堆的实现

堆通常用数组来实现(数组名一般为 h h h,即heap的首字母)。具体来讲,我们 1 1 1 开始,按照层序遍历的顺序给每个节点进行编号,例如,对于上图中的大根堆而言,其编号顺序如下:

深入理解堆与优先队列,数据结构与算法,# AcWing,数据结构,算法,c++,堆,优先队列

每个节点的编号就是该节点在数组中的下标,相应的数组为 h [    ] = { 0 , 10 , 7 , 6 , 4 , 5 , 1 , 2 } h[\;]=\{0,10,7,6,4,5,1,2\} h[]={0,10,7,6,4,5,1,2}(第 0 0 0 个元素是什么不重要)。

按照这种编号方式,不难发现:

  • 根节点的编号一定是 1 1 1
  • 若一个节点的编号为 x x x,则它左子节点(如果有)的编号为 2 x 2x 2x,右子节点(如果有)的编号为 2 x + 1 2x+1 2x+1
  • 若一个节点的编号为 x x x,则它父节点(如果有)的编号为 x / 2 x/2 x/2(这里的除法是整除)。

此外,根据完全二叉树的性质,还可以得到:

  • 若堆中含有 n n n 个元素,则堆的高度为 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor\log_2 n\rfloor+1 log2n+1
  • 若一个节点的编号为 x x x 且满足 x > n / 2 x>n/2 x>n/2(这里的除法是整除),则该节点一定是叶子节点,否则是分支节点。

2.1 上滤与下滤

⚠️ 为统一起见,接下来提到的堆均指小根堆

上滤(又称向上调整)和下滤(又称向下调整)是堆的两种基本操作。

上滤是指将不符合堆序性的某个元素向上调整至合适的位置,下滤是指将不符合堆序性的某个元素向下调整至合适的位置。

先来看下滤操作是如何进行的。设编号为 x x x 的节点不满足堆序性(该节点一定不是叶子节点,否则讨论将变得毫无意义),接下来分两种情况考虑:

  • 编号为 2 x 2x 2x 的节点存在,编号为 2 x + 1 2x+1 2x+1 的节点不存在: 这时候一定成立 h [ x ] > h [ 2 x ] h[x]>h[2x] h[x]>h[2x],此时交换 h [ x ] h[x] h[x] h [ 2 x ] h[2x] h[2x] 即可;
  • 编号为 2 x 2x 2x 的节点和编号为 2 x + 1 2x+1 2x+1 的节点均存在: 这时候 h [ x ] > h [ 2 x ] h[x]>h[2x] h[x]>h[2x] h [ x ] > h [ 2 x + 1 ] h[x]>h[2x+1] h[x]>h[2x+1] 中至少有一个成立。令 y = arg min ⁡ { h [ 2 x ] ,    h [ 2 x + 1 ] } y=\argmin \{h[2x],\;h[2x+1]\} y=argmin{h[2x],h[2x+1]},交换 h [ x ] h[x] h[x] h [ y ] h[y] h[y] 即可。

下滤操作的实现:

void down(int x) {
    while (x <= n / 2) {  // 当x不是叶子节点的时候持续向下调整
        int y = 2 * x;  // 如果x不是叶子节点,则至少存在左子节点
        if (y + 1 <= n && h[y + 1] < h[y]) y++;  // 判断左右子节点哪个更小,并令y等于更小的那个节点的编号
        if (h[y] >= h[x]) break;  // 如果左右子节点中的最小值都要大于等于节点x的值,说明x已经调整完毕
        swap(h[x], h[y]), x = y;  // 否则进行调整
    }
}

比起下滤操作,上滤操作的实现更为简单(因为往下走有两种选择:左、右子节点,而往上走只有一种选择:父节点)。设编号为 x x x 的节点不满足堆序性(该节点一定不是根节点,否则讨论将变得毫无意义),则一定有 h [ x ] < h [ x / 2 ] h[x]<h[x/2] h[x]<h[x/2],不断交换 h [ x ] h[x] h[x] h [ x / 2 ] h[x/2] h[x/2] 直至 h [ x ] ≥ h [ x / 2 ] h[x]\geq h[x/2] h[x]h[x/2] 即可。

上滤操作的实现:

void up(int x) {
    while (x > 1 && h[x] < h[x / 2]) {  // 当x不是根节点的时候持续向上调整
        swap(h[x], h[x / 2]);
        x /= 2;
    }
}

上滤操作和下滤操作的平均时间复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)

2.2 堆的常用操作

仅用上滤和下滤我们就可以实现堆的常用操作:

操作 时间复杂度
获取堆顶元素的值 O ( 1 ) O(1) O(1)
向堆中插入一个元素 O ( log ⁡ n ) O(\log n) O(logn)
删除堆顶元素 O ( log ⁡ n ) O(\log n) O(logn)
删除堆中的任一元素 O ( log ⁡ n ) O(\log n) O(logn)
修改堆中的任一元素 O ( log ⁡ n ) O(\log n) O(logn)

通常,我们需要用两个变量来表示一个堆:一个是上文提到的 h h h 数组,另一个是 i d x idx idx,用来表示当前堆中有多少个元素。

操作一:获取堆顶元素的值

int top() {
    return h[1];
}

操作二:向堆中插入一个元素

向堆中插入元素按照层序遍历的顺序进行,所以新插入的元素一定是叶子节点(编号最大的节点),此时对它进行上滤操作调整至合适的位置即可。

void push(int x) {
    h[++idx] = x, up(x);
}

操作三:删除堆顶元素

做法是用堆中最后一个元素(即编号最大的元素)覆盖掉堆顶元素,然后删除最后一个元素,同时下滤堆顶元素。

void pop() {
    h[1] = h[idx], idx--, down(1);
}

操作四:删除堆中的任一元素

不妨设要删除的元素的编号为 k k k,同样用最后一个元素覆盖掉这个元素,然后删除最后一个元素。此时对于编号为 k k k 的元素而言,要么执行上滤操作,要么执行下滤操作,要么什么都不用执行。简便起见,我们可以直接执行 down(k), up(k),这两个操作至多只有一个会被执行。

void pop(int k) {
    h[k] = h[idx], idx--, down(k), up(k);
}

可以看出 pop(1)pop() 等价。

操作五:修改堆中的任一元素

类似删除堆中的任一元素。

void modify(int k, int x) {
    h[k] = x, down(k), up(k);
}

2.3 建堆

给定一个乱序数组 a a a,我们如何根据它来建堆呢?

如果对于每一个 a [ i ] a[i] a[i],依次调用堆的 push 方法,则总时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),有没有更好的方法呢?

考虑将 a a a 赋值给 h h h(事实上一般不会这么做,而是直接输入到 h h h),此时 h h h 所代表的仅仅是完全二叉树,因为 h h h 不一定满足堆序性。对该完全二叉树的每个分支节点进行下滤(因为下滤叶子节点无意义)即可得到堆:

void build() {
    for (int i = idx / 2; i; i--) down(i);
}

下面分析 build 函数的时间复杂度。简便起见,不妨假设堆是满二叉树且含有 n n n 个元素,于是堆的高度为 h ≜ log ⁡ 2 ( n + 1 ) h\triangleq\log_2(n+1) hlog2(n+1)。规定根节点所在的层为第一层,于是最后一层的元素个数为 2 h − 1 2^{h-1} 2h1,倒数第二层的元素个数为 2 h − 2 2^{h-2} 2h2,以此类推。

build 从倒数第二层的节点开始逐个下滤,每个节点的操作次数至多是 1 1 1,因此 build 在倒数第二层的总操作次数为 2 h − 2 ⋅ 1 2^{h-2}\cdot 1 2h21

对于倒数第三层的节点,每个节点的操作次数至多是 2 2 2,因此 build 在倒数第三层的总操作次数为 2 h − 3 ⋅ 2 2^{h-3}\cdot 2 2h32

不断进行下去可得到 build 的总操作次数:

S = 2 h − 2 ⋅ 1 + 2 h − 3 ⋅ 2 + 2 h − 4 ⋅ 3 + ⋯ + 2 0 ⋅ ( h − 1 ) = ∑ i = 1 h − 1 i ⋅ 2 h − i − 1 \begin{aligned} S&=2^{h-2}\cdot 1+2^{h-3}\cdot 2+2^{h-4}\cdot 3+\cdots + 2^0\cdot (h-1)\\ &=\sum_{i=1}^{h-1}i\cdot 2^{h-i-1} \end{aligned} S=2h21+2h32+2h43++20(h1)=i=1h1i2hi1

经过简单计算可得:

S = 2 S − S = ∑ i = 1 h − 1 i ⋅ 2 h − i − ∑ i = 1 h − 1 i ⋅ 2 h − i − 1 = ∑ i = 1 h − 2 2 h − i + 1 + 2 h + 1 − ( h − 1 ) = 2 h + 2 − h − 7 = O ( 2 h ) = O ( n ) \begin{aligned} S&=2S-S=\sum_{i=1}^{h-1}i\cdot 2^{h-i}-\sum_{i=1}^{h-1}i\cdot 2^{h-i-1} \\ &=\sum_{i=1}^{h-2}2^{h-i+1}+2^{h+1}-(h-1) \\ &=2^{h+2}-h-7\\ &=O(2^h)=O(n) \end{aligned} S=2SS=i=1h1i2hii=1h1i2hi1=i=1h22hi+1+2h+1(h1)=2h+2h7=O(2h)=O(n)

故建堆的时间复杂度为 O ( n ) O(n) O(n)

三、堆排序

堆排序实际上就是先根据乱序序列建堆,然后将根节点与编号最大的节点进行交换(注意是交换而不是覆盖),同时下滤根节点。再将根节点与编号第二大的节点进行交换,同时下滤根节点,以此类推。

堆排序结束后,对堆进行层序遍历即可得到排序后的序列。

注意到如果初始时建立的是小根堆,则排序结束后会得到降序序列;如果初始时建立的是大根堆,则排序后会得到升序序列。

这里给出一个堆排序的模板:

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int n;  // 堆中的元素数量
int h[N];  // 用于存储堆的数组

// 大根堆的下滤操作
void down(int x) {
    while (x <= n / 2) {
        int y = 2 * x;
        if (y + 1 <= n && h[y + 1] > h[y]) y++;
        if (h[y] <= h[x]) break;
        swap(h[x], h[y]), x = y;
    }
}

int main() {
    cin >> n;

    for (int i = 1; i <= n; i++) cin >> h[i];  // 读入乱序序列

    for (int i = n / 2; i; i--) down(i);  // 建立大根堆

    int t = n;  // 循环结束后n的值会变为0,所以需要先提前保存一下方便后续输出
    while (n) {
        swap(h[1], h[n]), n--, down(1);
    }

    for (int i = 1; i <= t; i++) cout << h[i] << ' ';  // 输出升序序列

    return 0;
}

容易看出堆排序的时间复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度是 O ( 1 ) O(1) O(1)

四、优先队列

所谓优先队列,就是指定队列中元素的优先级,优先级越大越优先出队,而普通队列则是按照进队的先后顺序出队,可以看成进队越早越优先。

STL中的优先队列实际上就是大根堆,元素越大越优先出队。本节主要讲解STL中的优先队列的用法。

使用优先队列需要先包含头文件:

#include <queue>

创建一个优先队列(大根堆):

priority_queue<int> q;

如果要创建一个小根堆,则可以这样声明:

priority_queue<int, vector<int>, greater<int>> q;

优先队列的常用操作:

操作 描述
q.top() 返回队头元素
q.pop() 弹出队头元素
q.push(x) 向队列中插入元素
q.empty() 判断队列是否为空
q.size() 返回队列的大小

References

[1] https://oi-wiki.org/ds/heap/
[2] https://zh.cppreference.com/w/cpp/container/priority_queue
[3] https://zh.wikipedia.org/wiki/%E5%A0%86%E7%A9%8D
[4] https://www.acwing.com/activity/content/punch_the_clock/11/文章来源地址https://www.toymoban.com/news/detail-689498.html

到了这里,关于深入理解堆与优先队列的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【数据结构】优先级队列——堆

    🧧🧧🧧🧧🧧个人主页🎈🎈🎈🎈🎈 🧧🧧🧧🧧🧧数据结构专栏🎈🎈🎈🎈🎈 🧧🧧🧧🧧🧧【数据结构】非线性结构——二叉树🎈🎈🎈🎈🎈 前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能

    2024年04月14日
    浏览(62)
  • 数据结构——优先队列c++详解

    百度百科定义 优先队列是0个或多个元素的集合,每个元素都有一个优先权或值,对优先队列执行的操作有1) 查找;2) 插入一个新元素;3) 删除.在最小优先队列(min priority queue)中,查找操作用来搜索优先权最小的元素,删除操作用来删除该元素;对于最大优先队列(max priority queue),查找操

    2024年02月09日
    浏览(41)
  • 数据结构:优先级队列(堆)

    队列是一种先进先出 (FIFO) 的数据结构 ,但有些情况下, 操作的数据可能带有优先级,一般出队 列时,可能需要优先级高的元素先出队列。 在这种情况下, 数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象 。这种数据结构就是 优先级

    2024年02月08日
    浏览(42)
  • 数据结构与算法-优先级队列

    Gitee上开源的数据结构与算法代码库:数据结构与算法Gitee代码库 优先级队列,按照优先级别依次输出 计算机科学中,堆是一种基于树的数据结构,通常用 完全二叉树 实现。堆的特性如下 在大顶堆中,任意节点 C 与它的父节点 P 符合 P . v a l u e ≥ C . v a l u e P.value geq C.val

    2024年02月13日
    浏览(46)
  • 【数据结构与算法】03 队列(顺序队列--循环队列--优先级队列--链队列)

    队列( queue )是一种常见的数据结构,它遵循先进先出(FIFO)的原则。队列可以理解为一个具有两个端点的线性数据结构,其中一个端点称为\\\"队尾\\\"(rear),用于插入新元素,另一个端点称为\\\"队首\\\"(front),用于移除元素。新元素被插入到队尾,而最早插入的元素总是在队

    2024年02月08日
    浏览(55)
  • 数据结构 之 优先级队列(堆) (PriorityQueue)

    🎉欢迎大家观看AUGENSTERN_dc的文章(o゜▽゜)o☆✨✨ 🎉感谢各位读者在百忙之中抽出时间来垂阅我的文章,我会尽我所能向的大家分享我的知识和经验📖 🎉希望我们在一篇篇的文章中能够共同进步!!! 🌈个人主页:AUGENSTERN_dc 🔥个人专栏:C语言 | Java | 数据结构 ⭐个人

    2024年03月20日
    浏览(50)
  • 数据结构之优先级队列【堆】(Heap)

    目录 1. 优先级队列(Priority Queue) 2.堆的概念 3.堆的存储方式 4.堆的创建 5.用堆模拟实现优先级队列  6.PriorityQueue常用接口介绍 6.1 PriorityQueue的特点 6.2 PriorityQueue几种常见的构造方式 7.top-k问题 8.堆排序 本篇主要内容总结 (1)优先级队列底层是堆来实现的 (2)堆的本质是

    2024年02月01日
    浏览(56)
  • 数据结构 - 堆(优先队列)+ 堆的应用 + 堆练习

    1、本文章适合新学和复习用,都是用c语言实现的,包含了堆的讲解、堆的应用、堆的练习。 2、有图解和代码都注释,放心食用哦 那么开始: 一、什么是堆 堆(Heap)是计算机科学中一类特殊的数据结构,是最高效的优先级队列。堆通常是一个可以被看作一棵完全二叉树的数组

    2024年03月11日
    浏览(45)
  • 数据结构 - 6(优先级队列(堆)13000字详解)

    堆分为两种:大堆和小堆。它们之间的区别在于元素在堆中的排列顺序和访问方式。 大堆(Max Heap): 在大堆中,父节点的值比它的子节点的值要大。也就是说,堆的根节点是堆中最大的元素。大堆被用于实现优先级队列,其中根节点的元素始终是队列中最大的元素。 大堆

    2024年02月08日
    浏览(41)
  • 【数据结构】 优先级队列(堆)与堆的建立

    前面介绍过队列, 队列是一种先进先出(FIFO)的数据结构 ,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适。 比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话

    2024年02月10日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包