C++ STL容器(五) —— priority_queue 底层剖析

news2024/12/23 9:12:27

这篇来讲下 priority_queue,其属于 STL 的容器适配器,容器适配器是在已有容器的基础上修改活泼限制某些数据接口以适应更特定的需求,比如 stack 栈使数据满足后进先出,queue 队列使数据满足先进先出,其都是在已有容器上进行适配,以满足更细的需求。


文章目录

  • 堆的简单介绍
    • 堆的初始化
    • 堆的插入
    • 堆的删除
  • UML 类图
  • 代码解析
    • 构造函数
    • 插入元素
    • 删除元素


堆的简单介绍

堆是一棵二叉树,且是一棵完全二叉树,对于大根堆,根节点是这棵二叉树中最大的元素,也可以说每一棵子树的根节点都是这棵子树的最大元素,那么父节点一定是比左右孩子以及后代节点都大的。而小根堆,则是相反,根节点是这棵二叉树中最小的元素。

比如下图中左边是大根堆,而右边是小根堆。

堆的初始化

那么对于一个数组(随机排列的/随机生成的),我们要怎么把它转换大根堆/小根堆呢。

比如对于上图中的序列,可能初始序列是下图这样的。

那么根据这个初始序列将其转换成大根堆,可以采用从下至上的方式,先从右下角的子树开始调整,这个子树的根节点是 12 比它的孩子 1 大,所以不需要进行调整。

那么再调整左下角的子树,这个子树根节点是 3 比它的两个孩子都要小,那么和其最大的儿子交换,即 9,之后看交换完后,以 3 为根节点的子树是否还需要继续调整,而此时 3 已经是叶子节点了,没有左右孩子,所以不需要继续调整了,这轮调整结束。

然后再调整最上面的子树,8 为根节点,其比两个孩子节点要小,和其中最大的孩子交换,即 12,交换完后,其比孩子 1 小,所以不需要继续调整,本轮调整结束。

堆的插入

堆的插入操作,就是先插入到完全二叉树的下一个叶子节点,然后自下而上进行调整,而这个调整与堆初始化操作不一样的地方在于,如果父节点下沉了(即父节点和子节点发生交换),那么交换之后的下方子树不用再尝试进行调整了,因为当前父节点一定是这棵子树中的最大元素(大根堆)。

那么我们尝试在上面的大根堆中插入一个元素 14。

  1. 首先插入到完全二叉树的最后的叶子节点

  2. 调整以插入元素为儿子节点的子树

  3. 继续调整以 14 为儿子节点的子树,此时 14 已经是整棵大根堆的根节点了,不需要继续调整了。

堆的删除

堆的删除,是指把堆顶元素弹出, 通过把最末尾的叶子节点的元素放到根节点,然后删除最末的叶子节点,之后再从上至下进行调整即可。这里的调整与堆初始化操作的调整类似,如果根节点比两个孩子节点都大(大根堆),则不需要继续调整了,否则与其中最大的孩子交换,再继续向下调整子树。

  1. 将末尾的叶子拿到根节点,然后删除该叶子
  2. 8 比左右孩子都小,找到最大的孩子 12 交换,下面的子树 8 比 1 大,所以不需要继续调整了

UML 类图

下面看下 MSVC 实现中的 UML 类图,这里的容器可以是 vector 也可以是 list 等其它容器,这些容器要实现一些方法,因为在 priority_queue 里会使用,默认提供的是 vector,而 _Pr 提供的是比较函数,可以是仿函数,也可以是一个函数指针,_Pr 要实现给定两个参数的比较,默认提供的是 less 仿函数。


代码解析

构造函数

构造函数比较简单,就是把给定容器和比较函数初始化了,然后调用 _Make_heap 初始化堆。

priority_queue() = default;

explicit priority_queue(const _Pr& _Pred) noexcept(
    is_nothrow_default_constructible_v<_Container>
    && is_nothrow_copy_constructible_v<value_compare>) // strengthened
    : c(), comp(_Pred) {}

priority_queue(const _Pr& _Pred, const _Container& _Cont) : c(_Cont), comp(_Pred) {
    _Make_heap();
}

priority_queue(const _Pr& _Pred, _Container&& _Cont) : c(_STD move(_Cont)), comp(_Pred) {
    _Make_heap();
}

template <class _InIt, enable_if_t<_Is_iterator_v<_InIt>, int> = 0>
priority_queue(_InIt _First, _InIt _Last, const _Pr& _Pred, const _Container& _Cont) : c(_Cont), comp(_Pred) {
    c.insert(c.end(), _First, _Last);
    _Make_heap();
}

下面看下 _Make_heap 的代码,调用了 make_heap 函数,首先检查提供的两个迭代器,然后 _Make_heap_unchecked 进行堆的初始化

    void _Make_heap() {
        _STD make_heap(c.begin(), c.end(), _STD _Pass_fn(comp));
    }

	_EXPORT_STD template <class _RanIt, class _Pr>
_CONSTEXPR20 void make_heap(_RanIt _First, _RanIt _Last, _Pr _Pred) { // make [_First, _Last) into a heap
    _STD _Adl_verify_range(_First, _Last);
    _STD _Make_heap_unchecked(_STD _Get_unwrapped(_First), _STD _Get_unwrapped(_Last), _STD _Pass_fn(_Pred));
}

template <class _RanIt, class _Pr>
_CONSTEXPR20 void _Make_heap_unchecked(_RanIt _First, _RanIt _Last, _Pr _Pred) {
    // make [_First, _Last) into a heap
    using _Diff   = _Iter_diff_t<_RanIt>;
    _Diff _Bottom = _Last - _First;
    for (_Diff _Hole = _Bottom >> 1; _Hole > 0;) { // shift for codegen
        // reheap top half, bottom to top
        --_Hole;
        _Iter_value_t<_RanIt> _Val(_STD move(*(_First + _Hole)));
        _STD _Pop_heap_hole_by_index(_First, _Hole, _Bottom, _STD move(_Val), _Pred);
    }
}

因为序列的范围是 [_First, _Last) 所以这里的 _Hole = _Bottom >> 1 - 1 就是最下面的子树根节点的序号(相对于 _First 的偏移),然后通过 *(_First + _Hole) 将这个值拿到,再调用 _Pop_heap_hole_by_index 进行子树的调整,然后通过 --_Hole 依次向上调整子树即可,然后看下 _Pop_heap_hole_by_index 内部。

template <class _RanIt, class _Ty, class _Pr>
_CONSTEXPR20 void _Pop_heap_hole_by_index(
    _RanIt _First, _Iter_diff_t<_RanIt> _Hole, _Iter_diff_t<_RanIt> _Bottom, _Ty&& _Val, _Pr _Pred) {
    // percolate _Hole to _Bottom, then push _Val
    _STL_INTERNAL_CHECK(_Bottom > 0);

    using _Diff      = _Iter_diff_t<_RanIt>;
    const _Diff _Top = _Hole;
    _Diff _Idx       = _Hole;

    // Check whether _Idx can have a child before calculating that child's index, since
    // calculating the child's index can trigger integer overflows
    const _Diff _Max_sequence_non_leaf = (_Bottom - 1) >> 1; // shift for codegen
    while (_Idx < _Max_sequence_non_leaf) { // move _Hole down to larger child
        _Idx = 2 * _Idx + 2;
        if (_DEBUG_LT_PRED(_Pred, *(_First + _Idx), *(_First + (_Idx - 1)))) {
            --_Idx;
        }
        *(_First + _Hole) = _STD move(*(_First + _Idx));
        _Hole             = _Idx;
    }

    if (_Idx == _Max_sequence_non_leaf && _Bottom % 2 == 0) { // only child at bottom, move _Hole down to it
        *(_First + _Hole) = _STD move(*(_First + (_Bottom - 1)));
        _Hole             = _Bottom - 1;
    }

    _STD _Push_heap_by_index(_First, _Hole, _Top, _STD forward<_Ty>(_Val), _Pred);
}

template <class _RanIt, class _Ty, class _Pr>
_CONSTEXPR20 void _Push_heap_by_index(
    _RanIt _First, _Iter_diff_t<_RanIt> _Hole, _Iter_diff_t<_RanIt> _Top, _Ty&& _Val, _Pr _Pred) {
    // percolate _Hole to _Top or where _Val belongs
    using _Diff = _Iter_diff_t<_RanIt>;
    for (_Diff _Idx                                                          = (_Hole - 1) >> 1; // shift for codegen
         _Top < _Hole && _DEBUG_LT_PRED(_Pred, *(_First + _Idx), _Val); _Idx = (_Hole - 1) >> 1) { // shift for codegen
        // move _Hole up to parent
        *(_First + _Hole) = _STD move(*(_First + _Idx));
        _Hole             = _Idx;
    }

    *(_First + _Hole) = _STD forward<_Ty>(_Val); // drop _Val into final hole
}

这里的函数就是用来调整子树,和第一章讲解的调整方式有点不一样,这里是先把子树的根节点与最大的孩子节点交换,然后一直交换的叶子节点,然后从叶子节点开始向上调整,即如果比父节点大就交换,否则停止。_Pop_heap_hole_by_index 的前面部分就是交换到叶子节点,_Push_heap_by_index 就是向上调整的过程。

插入元素

插入元素使用 push,就是先调用容器的 push_back 方法把元素插入到末尾,然后 push_heap 调整堆

    void push(const value_type& _Val) {
        c.push_back(_Val);
        _STD push_heap(c.begin(), c.end(), _STD _Pass_fn(comp));
    }

    void push(value_type&& _Val) {
        c.push_back(_STD move(_Val));
        _STD push_heap(c.begin(), c.end(), _STD _Pass_fn(comp));
    }

	_EXPORT_STD template <class _RanIt, class _Pr>
_CONSTEXPR20 void push_heap(_RanIt _First, _RanIt _Last, _Pr _Pred) {
    // push *(_Last - 1) onto heap at [_First, _Last - 1)
    _STD _Adl_verify_range(_First, _Last);
    const auto _UFirst = _STD _Get_unwrapped(_First);
    auto _ULast        = _STD _Get_unwrapped(_Last);
    using _Diff        = _Iter_diff_t<_RanIt>;
    _Diff _Count       = _ULast - _UFirst;
    if (2 <= _Count) {
        _Iter_value_t<_RanIt> _Val(_STD move(*--_ULast));
        _STD _Push_heap_by_index(_UFirst, --_Count, _Diff(0), _STD move(_Val), _STD _Pass_fn(_Pred));
    }
}

push_heap 里就是调用上面提到的 _Push_heap_by_index 从叶子节点向上调整,这里 2 <= _Count 就是如果只有 1 个元素肯定就不用调整了。

删除元素

删除元素就是先调用 pop_heap,然后调用容器的 pop_back 删除末尾的元素。

    void pop() {
        _STD pop_heap(c.begin(), c.end(), _STD _Pass_fn(comp));
        c.pop_back();
    }

_Pop_heap_unchecked 中里的 2 <= _Last - _First 判断也是当堆里只有一个元素的时候,就不需要调整了,直接容器 pop_back 掉就可以了。
_Pop_heap_hole_unchecked 里的 *_Dest = _STD move(*_First) 就是把根节点放到末尾节点,_Val 里保存了之前的末尾元素,然后 _Pop_heap_hole_by_index 和堆初始化里用的一样,把更大的孩子节点依次上移,然后再把之前的末尾元素的值向上调整。

_EXPORT_STD template <class _RanIt, class _Pr>
_CONSTEXPR20 void pop_heap(_RanIt _First, _RanIt _Last, _Pr _Pred) {
    // pop *_First to *(_Last - 1) and reheap
    _STD _Adl_verify_range(_First, _Last);
    _STD _Pop_heap_unchecked(_STD _Get_unwrapped(_First), _STD _Get_unwrapped(_Last), _STD _Pass_fn(_Pred));
}

template <class _RanIt, class _Pr>
_CONSTEXPR20 void _Pop_heap_unchecked(_RanIt _First, _RanIt _Last, _Pr _Pred) {
    // pop *_First to *(_Last - 1) and reheap
    if (2 <= _Last - _First) {
        --_Last;
        _Iter_value_t<_RanIt> _Val(_STD move(*_Last));
        _STD _Pop_heap_hole_unchecked(_First, _Last, _Last, _STD move(_Val), _Pred);
    }
}
template <class _RanIt, class _Ty, class _Pr>
_CONSTEXPR20 void _Pop_heap_hole_unchecked(_RanIt _First, _RanIt _Last, _RanIt _Dest, _Ty&& _Val, _Pr _Pred) {
    // pop *_First to *_Dest and reheap
    // precondition: _First != _Last
    // precondition: _First != _Dest
    *_Dest      = _STD move(*_First);
    using _Diff = _Iter_diff_t<_RanIt>;
    _STD _Pop_heap_hole_by_index(
        _First, static_cast<_Diff>(0), static_cast<_Diff>(_Last - _First), _STD forward<_Ty>(_Val), _Pred);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2197477.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【重学 MySQL】六十一、数据完整性与约束的分类

【重学 MySQL】六十一、数据完整性与约束的分类 数据完整性什么是约束约束的分类如何查看、添加和删除约束查看约束添加约束删除约束 在MySQL中&#xff0c;数据完整性是确保数据库中数据的准确性和一致性的关键。为了实现数据完整性&#xff0c;MySQL提供了多种约束类型&#…

【Qt】窗口预览(1)—— 菜单栏

窗口预览&#xff08;1&#xff09; 1. QMainWindow2. QMenuBar——菜单栏2.1 创建菜单栏/将菜单栏添加到widget中2.2 addMenu——在菜单栏中添加菜单2.3 在菜单中添加选项2.4 添加快捷键2.5 支持嵌套添加菜单2.6 添加信号2.7 添加分割线 1. QMainWindow Qt窗口是通过QMainWin…

插件-发送邮件通知

有时候通过python运行程序&#xff0c;在出现异常时&#xff0c;需要进行邮件通知&#xff0c;可能还需要截图。比如对浏览器进行控制时出现了异常&#xff0c;则需要进行截图分析。 email-validator 2.0.0.post2 import asyncio import logging import smtpli…

C++基础面试题 | C++中野指针和悬挂指针的区别?

文章目录 回答重点&#xff1a;1. 野指针&#xff08;Wild Pointer&#xff09;&#xff1a;2. 悬挂指针&#xff08;Dangling Pointer&#xff09;&#xff1a; 拓展知识&#xff1a;如何避免这些问题野指针和悬挂指针 回答重点&#xff1a; 在C中&#xff0c;野指针是指未初…

职场上的人情世故,你知多少?这五点一定要了解

职场是一个由人组成的复杂社交网络&#xff0c;人情世故在其中起着至关重要的作用。良好的人际关系可以帮助我们更好地融入团队&#xff0c;提升工作效率&#xff0c;甚至影响职业发展。在职场中&#xff0c;我们需要了解一些关键要素&#xff0c;以更好地处理人际关系&#xf…

计算机网络:物理层 —— 信道复用技术

文章目录 信道信道复用技术信道复用技术的作用基本原理常用的信道复用技术频分复用 FDM时分复用 TDM波分复用 WDM码分复用 CDM码片向量基本原理 信道 信道是指信息传输的通道或介质。在通信中&#xff0c;信道扮演着传输信息的媒介的角色&#xff0c;将发送方发送的信号传递给…

输入三位数的整数,求最大的一位数字 python

题目&#xff1a; 输入三位数整数&#xff0c;求最大的一位数字 代码&#xff1a; aint(input("请输入三位正整数&#xff1a;")) xa%10 #个 ya//10%10 #十 za//100%10 #百 print("最大的一位数为", max(x,y,z))运行结果&#xff1a;

20.Nginx动静分离原理与案例实现

一.Nginx动静分离原理与案例实现 1.动静分离原理图 2.动静分离的问题 3. Nginx动静分离案例实践 3.1 nginx部署架构图 3.2 nginx部署案例实现 (1)配置tomcats.conf文件 api.z.mukewang.com反向代理tomcat api upstream tomcats {server

AI编程工具的机遇与风险

作者 吴国平 北京市隆安律师事务所 超过1万个程序员&#xff0c;77,000个项目使用了Copilot&#xff0c;55%的程序员选择Copilot。 程序员使用人工智能来协助编写代码时&#xff0c;最终作品的所有权就变成了一个灰色地带。传统的软件著作权法是在程序员是代码创作…

Linux高效查日志命令介绍

说明&#xff1a;之前介绍Linux补充命令时&#xff0c;有介绍使用tail、grep命令查日志&#xff1b; Linux命令补充 今天发现仅凭这两条命令不够&#xff0c;本文扩展介绍一下。 命令一&#xff1a;查看日志开头 head -n 行数 日志路径如下&#xff0c;可以查看程序启动是否…

Django一分钟:DRF生成OpenAPI接口文档

DRF项目中如果想要自动生成API文档我们可以借助drf-spectacular这个库&#xff0c;drf-spectacular非常强大&#xff0c;它可以自动从DRF中提取信息&#xff0c;自动生成API文档&#xff0c;配置简单开箱即用&#xff0c;并且它对很多常用的第三方如&#xff1a;SimpleJWT、dja…

专业高清录屏软件!Mirillis Action v4.40 解锁版下载,小白看了都会的安装方法

Mirillis Action!&#xff08;暗神屏幕录制软件&#xff09;专业高清屏幕录像软件&#xff0c;被誉为游戏视频三大神器之一。这款屏幕录制软件和游戏录制软件&#xff0c;拥有三大硬件加速技术&#xff0c;支持以超高清视频画质录制桌面和实况直播&#xff0c;超清视频画质&…

论文速读:基于渐进式转移的无监督域自适应舰船检测

这篇文章的标题是《Unsupervised Domain Adaptation Based on Progressive Transfer for Ship Detection: From Optical to SAR Images》基于渐进式转移的无监督域自适应舰船检测:从光学图像到SAR图像&#xff0c;作者是Yu Shi等人。文章发表在IEEE Transactions on Geoscience…

erlang学习:Linux命令学习9

sed命令介绍 sed全称是&#xff1a;Stream EDitor&#xff08;流编辑器&#xff09; Linux sed 命令是利用脚本来处理文本文件&#xff0c;sed 可依照脚本的指令来处理、编辑文本文件。Sed 主要用来自动编辑一个或多个文件、简化对文件的反复操作、编写转换程序等 sed 的运行…

Dev-C++ 安装与使用(dev c++官网)(已解决)

1.Dev-C的安装 ①打开Dev-C的官网(https://sourceforge.net/projects/orwelldevcpp/ )&#xff1b;点击Download(下载)&#xff0c;等待5秒后开始下载。 ②点开下载好的EXE文件&#xff0c;等待加载完成(如图)。 右键&#xff0c;以管理员身份 运行安装包。 选择English(英语),…

近年来自动驾驶行业就业与企业需求情况

自动驾驶行业在近年来持续发展&#xff0c;就业情况和企业需求呈现出多样化和复杂化的趋势。 以下是基于我搜索到的资料对自动驾驶行业最新就业情况和企业需求的详细分析&#xff1a; 自动驾驶行业对高端技术人才的需求非常旺盛&#xff0c;尤其是架构工程师、算法工程师等岗…

四、Python基础语法(数据类型转换)

数据类型转换就是将一种类型的数据转换为另外一种类型的数据&#xff0c;数据类型转换不会改变原数据&#xff0c;是产生一个新的数据。 变量 要转换为的类型(原数据) -> num int(28) 一.int()将其他类型转换为整型 1.整数类型的字符串转换为整型 num1 28 print(type…

判断推理(3)

A正好说反了 C没说唐朝是否使用陶片 题干说的是有时会造成伤害&#xff0c;但是没有说服用了维生素和矿物质一定会带来伤害&#xff0c;所以A选项不能进行削弱 D是对比实验:增加反向论据。通过对儿童的调查发现&#xff0c;不服用的儿童营养缺乏症的发病率高&#xff0c;通过对…

Windows无需管理员权限,命令轻松修改IP和DNS

哈喽大家好&#xff0c;欢迎来到虚拟化时代君&#xff08;XNHCYL&#xff09;。 “ 大家好&#xff0c;我是虚拟化时代君&#xff0c;一位潜心于互联网的技术宅男。这里每天为你分享各种你感兴趣的技术、教程、软件、资源、福利…&#xff08;每天更新不间断&#xff0c;福利…