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

这篇来讲下 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 heapusing _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 overflowsconst _Diff _Max_sequence_non_leaf = (_Bottom - 1) >> 1; // shift for codegenwhile (_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 belongsusing _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 reheapif (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.rhkb.cn/news/442566.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

转型AI产品经理需要掌握的硬知识、经理能力模型和常见AI概念梳理

近几年&#xff0c;从亚马逊&#xff0c; Facebook&#xff0c;到谷歌&#xff0c;微软&#xff0c;再到国内的BAT&#xff0c;全球最具影响力的技术公司都将目光转向了人工智能&#xff08; AI &#xff09;。2016年 AlphaGo 战胜李世石&#xff0c;把公众的目光也聚集到了人工…

哪些因素会影响PMC对生产质量问题的响应速度?

在制造业中&#xff0c;生产物料控制&#xff08;PMC&#xff09;扮演着至关重要的角色&#xff0c;它负责协调生产计划、物料采购、库存管理和生产进度等多个环节&#xff0c;确保生产活动能够顺利进行。然而&#xff0c;面对生产过程中可能出现的各种质量问题&#xff0c;PMC…

详解 PDF 转 JPG:简单操作,高效转换

如今&#xff0c;众多软件都已具备将PDF转换为JPG的功能&#xff0c;所以pdf怎么转换成jpg图片已经不难解决了吧。接下来&#xff0c;我想分享几款依然保存在我电脑中&#xff0c;且非常实用的PDF转JPG工具给大家。 1.福昕PDF转换大师 链接一下>>https://www.pdf365.cn…

C语言基础之结构体

今天我们来讲讲C语言基础的最后一个知识点了 —— 结构体。不知道大家对前面的C语言基础的知识点掌握的怎么样了呢&#xff1f;下面我们就开始讲解结构体的相关知识点吧&#xff01; 什么是结构体呢&#xff1f;或者说结构体有什么作用呢&#xff1f;对于复杂对象来说&#xff…

盘点2024年4款打工人都在用的PDF软件。

PDF 软件在现代的办公或者是学习当中的应用非常广泛&#xff0c;已经成了很多人的必备工具。因为PDF 文件具有跨设备、跨系统的优势&#xff0c;所以在很多设备上都可以打开浏览。如果有了PDF 编辑软件&#xff0c;查看&#xff0c;编辑&#xff0c;分享也会变得更加方便简单&a…

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

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

DAMA数据管理知识体系(第9章 文件和内容管理)

课本内容 9.1 引言 概要 文件和内容管理是指针对存储在关系型数据库之外的数据和信息的采集、存储、访问和使用过程的管理[1]。它的重点在于保持文件和其他非结构化或半结构化信息的完整性&#xff0c;并使这些信息能够被访问。业务驱动因素 法规遵从性要求 法律法规要求组织保…

每日OJ题_牛客_平方数_数学_C++_Java

目录 牛客_平方数_数学 题目解析 C代码1暴力 C代码2数学 Java代码数学 牛客_平方数_数学 平方数 (nowcoder.com) 描述&#xff1a; 牛妹是一个喜欢完全平方数的女孩子。 牛妹每次看到一个数 x&#xff0c;都想求出离 x 最近的完全平方数 y。 每次手算太麻烦&#xff0c;…

LeetCode讲解篇之322. 零钱兑换

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们可以使用动态规划解决这道题&#xff0c;我们首先定义一个数组&#xff0c;数组中第i个元素表示组成金额 i 的最少硬币个数 我们遍历数组的1 ~ amount号位置&#xff0c;对coins进行遍历&#xff0c;查找选…

Chromium 搜索引擎功能浅析c++

地址栏输入&#xff1a;chrome://settings/searchEngines 可以看到 有百度等数据源&#xff0c;那么如何调整其顺序呢&#xff0c;此数据又存储在哪里呢&#xff1f; 1、浏览器初始化搜索引擎数据来源在 components\search_engines\prepopulated_engines.json // Copyright …

【C语言刷力扣】1678.设计Goal解析器

题目&#xff1a; 解题思路&#xff1a; 遍历分析每一个字符&#xff0c;对不同情况分别讨论。 若是字符 G &#xff0c;则 res 中添加字符 G若是字符 &#xff08; &#xff0c;则再分别讨论。 若下一个字符是 &#xff09;&#xff0c; 则在 res 末尾添加字符 o若下一个字符…

【CSS in Depth 2 精译_045】7.1 CSS 响应式设计中的移动端优先设计原则(上)

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一章 层叠、优先级与继承&#xff08;已完结&#xff09; 1.1 层叠1.2 继承1.3 特殊值1.4 简写属性1.5 CSS 渐进式增强技术1.6 本章小结 第二章 相对单位&#xff08;已完结&#xff09; 2.1 相对…

分布式锁--redission 最佳实践!

我们知道如果我们的项目服务不只是一个实例的时候&#xff0c;单体锁就不再适用&#xff0c;而我们自己去用redis实现分布式锁的话&#xff0c;会有比如锁误删、超时释放、锁的重入、失败重试、Redis主从一致性等等一系列的问题需要自己解决。 当然&#xff0c;上述问题并非无…

刷题 二叉树

二叉树的核心思想 - 递归 - 将问题分解为子问题 题型 递归遍历迭代遍历层序遍历 bfs&#xff1a;队列各种递归题目&#xff1a;将问题分解为子问题二叉搜索树 - 中序遍历是递增序列 TreeNode* &prev 指针树形dp 面试经典 150 题 - 二叉树 104. 二叉树的最大深度 广度优…

DDD简介

概述 传统的数据驱动开发模式&#xff0c;View、Service、Dao这种三层分层模式&#xff0c;会很自然的写出过程式代码&#xff0c;这种开发方式中的对象只是数据载体&#xff0c;而没有行为&#xff0c;是一种贫血对象模型。以数据为中心&#xff0c;以数据库ER图为设计驱动&a…

JavaSE - 基础语法

01 背景知识补充 ① Java统治了后台服务器的开发&#xff0c;比如京东&#xff0c;淘宝网站的后台服务器就是使用的Java进行开发的 ② Java之父&#xff1a;詹姆斯高斯林 ③ Java由sun公司研发&#xff0c;现在属于Oracle公司 02 注释 ① Java的注释有三种&#xff1a;单行…

快速启动工具 | Biniware Run v7.1.0.0 绿色中文版

Biniware Run是一款便携式的Windows生产力工具&#xff0c;旨在为用户提供快速访问其喜爱的网站地址、文件和文件夹的便捷方式。这款软件的特点在于其易用性和高度可定制性。用户可以通过简单的拖放操作&#xff0c;将网址、文件或文件夹添加到软件中&#xff0c;从而快速访问。…

网络层协议 --- IP

序言 在这篇文章中我们将介绍 IP协议&#xff0c;经过这篇文章的学习&#xff0c;我们就会了解运营商到底是如何为我们提供服务的以及平时我们所说的内网&#xff0c;公网到底又是什么&#xff0c;区别是什么&#xff1f; IP 地址的基本概念 1. IP 地址的定义 每一个设备接入…

【进阶OpenCV】 (4)--图像拼接

文章目录 图像拼接1. 读取图片2. 计算图片特征点及描述符3. 建立暴力匹配器4. 特征匹配5. 透视变换6. 图像拼接 总结 图像拼接 图像拼接是一项将多张有重叠部分的图像&#xff08;这些图像可能是不同时间、不同视角或者不同传感器获得的&#xff09;拼成一幅无缝的全景图或高分…

AI学习记录 - L2正则化详细解释(权重衰减)

大白话&#xff1a; 在反向传播时&#xff0c;加入额外的损失值&#xff0c;让总损失值变得比原来更大&#xff0c;并且加入的损失值要关联到神经网络全部权重的大小&#xff0c;当出现权重的平方变大的时候&#xff0c;也就是网络权重往更加负或者更加正的方向走的时候&#…