d-ary heap实现一个快速的优先级队列(C#)
d-ary heap简介:
d-ary heap 是泛化版本的binary heap(d=2),d-ary heap每个非叶子节点最多有d个孩子结点。
d-ary heap拥有如下属性:
- 类似complete binary tree,除了树的最后一层,其它层全部填满结点,且增加结点方式由左至右。
- 类似binary heap,它也分两类最大堆和最小堆。
下面给出一个3-ary heap示例:
3-ary max heap - root node is maximum of all nodes10/ | \7 9 8/ | \ /4 6 5 73-ary min heap -root node is minimum of all nodes10/ | \12 11 13/ | \14 15 18
具有n个节点的完全d叉树的高度由logdn给出。
d-ary heap的应用:
d-ary heap常用于进一步实现优先级队列,d-ary heap实现的优先级队列比用binary heap实现的优先队列在添加新元素的方面效率更高。binary heap:O(log2n) vs d-ary heap: O(logkn) ,当d > 2 时,logkn < log2n 。但是d-ary heap实现的优先级队列缺点是提取优先级队列首个元素比binary heap实现的优先队列需要消耗更多性能。binary heap:O(log2n) vs d-ary heap:O((d-1)logdn),当 d > 2 时,(d-1)logdn > log2n ,通过对数换底公式可证。结果看起来喜忧参半,那么什么情况下特别适合使用d-ary heap呢?答案就是游戏中常见的寻路算法。就以A*和Dijkstra algorithm举例。两者一般都需要一个优先级队列(有某些A*算法不适用优先级队列,比如迭代加深A*),而这些算法在取出队列首个元素时,往往要向队列中添加更多的临近结点。也就是添加结点次数远远大于提取次数。那么正好,d-ary heap可以取长补短。另外,d-ary heap比binary heap 对缓存更加友好,更多的子结点相邻在一起。故在实际运行效率往往会更好一些。
d-ary heap及优先级队列的实现:
我们用数组实现d-ary heap,数组以0为起始,可以得到如下规律:
- 若该结点为非根结点,那么使用该结点的索引i可以取得其的父结点索引,父结点为(i-1)/d;
- 若该结点的索引为i,那么它的孩子结点索引分别为(d*i)+1 , (d*i)+2 …. (d*i)+d;
- 若heap大小为n,最后一个非叶子结点的索引为(n-1)/d;(注:本文给出的实现并没有使用该规则)
构建d-ary heap堆:本文给出的实现侧重于进一步实现优先级队列,并采用最小堆(方便适配寻路算法)。所以把一个输入数组堆化,并不是核心操作,为了方便撰写代码以及加强可读性,构建堆算法采用从根结点至下方式,而不是从最后一个非叶子结点向上的方式。优点显而易见,代码清晰,不需要使用递归且不需要大量if else语句来寻找最小的孩子结点。只要孩子结点的值小于其父节点将其交换即可。缺点显而易见,交换次数增加从而降低效率。
public void BuildHeap() {for (int i = 1; i < numberOfItems; i++) {int bubbleIndex = i;ar node = heap[i];while (bubbleIndex != 0) {int parentIndex = (bubbleIndex-1) / D;if (node.CompareTo(heap[parentIndex]) < 0) {heap[bubbleIndex] = heap[parentIndex];heap[parentIndex] = node;bubbleIndex = parentIndex;} else {break;}}}}
Push:向优先级队列中添加新的元素,若添加node为空,抛出异常,若空间不足,则扩展空间。最后调用内部函数DecreaseKey加入新的结点到d-ary heap。
public void Push(T node) {if (node == null) throw new System.ArgumentNullException("node");if (numberOfItems == heap.Length) {Expand();}DecreaseKey(node, (ushort)numberOfItems);numberOfItems++; }
DecreaseKey:传入的index为当前队列中现有元素的数量。这个函数是私有的,因为对于优先级队列来说并不需要提供改接口。这里我们使用了一个优化技巧,暂不保存待加入的结点到数组,直到我们找到了它在数组中的合适位置,这样可以节省不必要的交换。
private void DecreaseKey (T node, ushort index){if(index < numberOfItems){if(node.CompareTo(heap[index]) > 0 ){throw new System.Exception("New node key greater than orginal key");}}int bubbleIndex = index;while (bubbleIndex != 0) {// Parent node of the bubble nodeint parentIndex = (bubbleIndex-1) / D;if (node.CompareTo(heap[parentIndex]) < 0 ) {// Swap the bubble node and parent node// (we don't really need to store the bubble node until we know the final index though// so we do that after the loop instead)heap[bubbleIndex] = heap[parentIndex];bubbleIndex = parentIndex;} else {break;}}heap[bubbleIndex] = node; }
Pop:弹出优先级队列top元素,调用内部函数ExtractMin。
public T Pop () {return ExtractMin(); }
ExtractMin:返回当前root node,更新numberOfItems,重新堆化。把最后一个叶子结点移动到root node,结点依照规则上浮。这里使用了同样的优化技巧。不必把最后一个叶子结点保存到数组0的位置,等到确定其最终位置再把它存入数组。这样做的好处节省交换次数。
private T ExtractMin() {T returnItem = heap[0];numberOfItems--;if (numberOfItems == 0) return returnItem;// Last item in the heap arrayvar swapItem = heap[numberOfItems];int swapIndex = 0, parent;while (true) {parent = swapIndex;var curSwapItem = swapItem;int pd = parent * D + 1;// If this holds, then the indices used// below are guaranteed to not throw an index out of bounds// exception since we choose the size of the array in that wayif (pd <= numberOfItems) {for(int i = 0;i<D-1;i++){if (pd+i < numberOfItems && (heap[pd+i].CompareTo(curSwapItem) < 0)){curSwapItem = heap[pd+i];swapIndex = pd+i;}}if (pd+D-1 < numberOfItems && (heap[pd+D-1].CompareTo(curSwapItem) < 0)) {swapIndex = pd+D-1;}}// One if the parent's children are smaller or equal, swap them// (actually we are just pretenting we swapped them, we hold the swapData// in local variable and only assign it once we know the final index)if (parent != swapIndex) {heap[parent] = heap[swapIndex];} else {break;}}// Assign element to the final positionheap[swapIndex] = swapItem;// For debugging Validate ();return returnItem;}
时间复杂度分析:
- 对于用d ary heap实现的优先级队列,若队列拥有n个元素,其对应堆的高度最大为logdn ,添加新元素时间复杂度为O(logdn)
- 对于用d ary heap实现的优先级队列,若队列拥有n个元素,其对应堆的高度最大为logdn,要在d个孩子结点当中选取最小或最大结点,层层不断上浮。故删除队首元素时间复杂度为(d-1)logdn
- 对于把数组转化为d ary heap,采用从最后一个非叶子结点向上的方式,其时间复杂度为O(n),分析思路和binary heap一样。举例说明,对于拥有n个结点的4 ary heap,高度为1子树的有(3/4)n,高度为2的子树有(3/16)n... 处理高度为1的子树需要O(1),处理高度为2的子树需要O(2)... 累加公式为 $\sum_{k=1}^{log_{4}^{n}}{\frac{3}{4^{k}}}nk$ ,根据比值收敛法可知这个无穷级数是收敛的,故复杂度仍为O(n)。那么对于本文给出的自顶向下的方式,其复杂度又如何呢?答案为O($dlog_{d}^{n}n$),具体的运算过程(详见下一条),理论上时间复杂度要高于采用从最后一个非叶子结点向上的方式。但两者实际效率相差多少需进行实际测试。
- 本文的buildheap算法,第i层的结点至多需要比较和交换i次,且第i层结点数di,由此可得时间统计范式为$\sum_{i=1}^{log_{d}^{n}}{d^{i}}i$,以d=4为例 $\sum_{i=1}^{log_{4}^{n}}{4^{i}}i$。需要求前i项和Si关于i的表达式,Si= 1*4 +2*42+3*43+.....+ i*4i ,那么4Si=1*42+2*43+......+i*4i+1,用4Si-Si进行错位相减,得知3Si=i*4i+1 - (4+42+......+4i) 。痛快,后者是一个等比数列。这样整个式子最后表达为$Si=\frac{4}{9}+\frac{1}{3}(i-\frac{1}{3})4^{i+1}$,我们知道i值为logdn,代入可得O($dlog_{d}^{n}n$)。
总结:
通过使用System.Diagnostics.Stopwatch 进行多次测试,发现d=4 时,push和pop的性能都不错,d=4很多情况下Push都比d=2的情况要好一些。push可以确定性能确实有所提高,pop不能确定到底是好了还是坏了,实验结果互有胜负。说到底System.Diagnostics.Stopwatch并不是精确测试,里面还有.net的噪音。
附录:
优先级队列完整程序
Q&A:
Q:
我的寻路算法想要使用C++或Java标准库自带的PriorityQueue,两者都没有提供DecreaseKey函数,带来的问题是我无法更新队列里元素key,没有办法进行边放松,如何处理?
A:
笔者文章DecreaseKey也是私有的,没有提供给PriorityQueue的使用者。为什么不提供呢?因为即便提供了寻路算法如何给出DecreaseKey所需的index呢?我们知道需要更新的元素在优先级队列中,但是index并不知道,要获取index就需要进行搜索(或者使用额外数据结构辅助)。使用额外的数据结构辅助确定index必然占用更多内存空间,使用搜索确定index必然消耗更多时间尤其是当队列中元素很多时。诀窍根本不改变它。而是将该节点的 "新建副本 " (具有新的更好的成本) 添加到优先级队列中。由于成本较低, 该节点的新副本将在队列中的原始副本之前提取, 因此将在前面进行处理。后面遇到的重复结点直接忽略即可,并且很多情况还没等到处理重复结点时我们已经找到路径了。我们所额外负担的就是优先级队列中存在一些多余对象。这种负担非常小,而且实现起来简便。
参考文献:
https://www.geeksforgeeks.org/k-ary-heap/
http://en.wikipedia.org/wiki/Binary_heap
https://en.wikipedia.org/wiki/D-ary_heap
欢迎评论区交流,批评,指正~
原创文章,转载请标明出处,谢谢~
转载于:https://www.cnblogs.com/tangzhenqiang/p/9508667.html
d-ary heap实现一个快速的优先级队列(C#)相关推荐
- python优先级排序_Python实现一个优先级队列的方法
问题 怎样实现一个按优先级排序的队列? 并且在这个队列上面每次 pop 操作总是返回优先级最高的那个元素 解决方案 下面的类利用 heapq 模块实现了一个简单的优先级队列: import heapq ...
- 《Python Cookbook 3rd》笔记(1.5):实现一个优先级队列
实现一个优先级队列 问题 怎样实现一个按优先级排序的队列?并且在这个队列上面每次pop操作总是返回优先级最高的那个元素. 解法 下面的类利用 heapq 模块实现了一个简单的优先级队列: import ...
- 二叉堆详解实现优先级队列
二叉堆详解实现优先级队列 文章目录 二叉堆详解实现优先级队列 一.二叉堆概览 二.优先级队列概览 三.实现 swim 和 sink 四.实现 delMax 和 insert 五.最后总结 二叉堆(Bi ...
- 【STL学习】优先级队列Priority Queue详解与C++编程实现
优先级队列Priority Queue介绍 优先级队列是一个拥有权值观念的queue.它允许在底端添加元素.在顶端去除元素.删除元素. 优先级队列内部的元素并不是按照添加的顺序排列,而是自动依照元素的 ...
- C++数据结构与算法(九) 树,优先级队列,最大堆的实现
树: 用来表示具有结构层次的数据,应用: 软件工程技术:模块化技术 根: 子树: 在树中,每个元素都代表一个节点. 树的级: 根是一级,根的孩子是二级,一次往下,有三级,四级... 树的高度(深度): ...
- Go实战 | 一文带你搞懂从单队列到优先级队列的实现
大家好,我是渔夫子,今天跟大家聊聊在我们项目中的优先级队列的实现. 优先级队列概述 队列,是数据结构中实现先进先出策略的一种数据结构.而优先队列则是带有优先级的队列,即先按优先级分类,然后相同优先 ...
- 【数据结构Python描述】优先级队列描述“银行VIP客户插队办理业务”及“被插队客户愤而离去”的模型实现
文章目录 一.支持插队模型的优先级队列 队列ADT扩充 队列记录描述 方法理论步骤 `update(item, key, value)` `remove(item)` 二.支持插队模型的优先级队列实现 ...
- 二叉堆(TopK问题,优先级队列)
目录 实现一个大根堆 优先级队列 Comparable和Compator区别 compareTo方法 TopK问题 TopK问题常见题型为求最大(最小)的K个值. 我们一般拿堆来解决. 堆:二叉堆首先 ...
- 优先级队列 c语言,队列优先级
优先级队列比队列更专业的数据结构.像普通队列,优先级队列中有相同的方法,但在使用上是有比较大的区别的.在优先级队列数据项都受到键值排序,以便与最低键的值,数据项在前方,键的最高值的数据项在后方,反之亦 ...
最新文章
- CMD——ping及用其检测网络故障
- pfSense设置多WAN后,解决网银无法登陆问题
- SharePoint 2010 与 SQL Server 2012 报表服务集成
- 使用 cout 输出数据之控制输出格式(二)
- 正则表达式(基础、常用)----JavaScript
- Java静态域与静态方法
- 社会工程学***的八种常用方法
- frp + nginx 配置多人共用的http 内网穿透服务
- 近7成开发者无开源收入、最想操作系统开源、Java最受欢迎 | 揭晓中国开源开发者现状...
- linux软件包管理系统的意义,Linux系统的软件包管理——RPM
- 什么叫pmt测试分析_圆偏振发光光谱仪——南方科技大学分析测试中心设备介绍第51期...
- 数据结构上机实践第八周项目6- 猴子选大王(数组版)
- 正好股票资讯大盘平衡被打破
- 台北故宫博物院收藏:气势开张,米芾行草书法真迹《真酥帖》赏析
- demonstration记忆_记忆英语单词方法20种
- go用函数字符串名调用函数
- 单点登录(SSO)、CAS介绍
- 自己收款码实现个人网站支付
- css表格nth左对齐,使用CSS nth-child选择单个表格单元格
- 高并发下的幂等策略分析
热门文章
- SpringCloud系列之版本选择
- 阿呆喵广告过滤 v1.9.0.1 官网版
- IObit Uninstaller 10Pro BD
- Wikipedia iOS客户端源码
- [Done]FindBugs: boxing/unboxing to parse a primitive
- Idea中maven 只从本地仓库导入jar包,取消联网下载的问题
- Flatten()详解
- 使用仿射变换将一幅图像放置到另一幅图像中
- 手工搭建多层(多隐藏层)BP神经网络
- Qt 资源图片删除后,错误 needed by `debug/qrc_image.cpp'. Stop. 的终极解决办法