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 nodes
10
/ | \
7 9 8
/ | \ /
4 6 5 7 3-ary min heap -root node is minimum of all nodes
10
/ | \
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 = ; i < numberOfItems; i++)
{
int bubbleIndex = i;
ar node = heap[i]; while (bubbleIndex != )
{
int parentIndex = (bubbleIndex-) / D; if (node.CompareTo(heap[parentIndex]) < )
{
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]) > )
{
throw new System.Exception("New node key greater than orginal key");
}
}
int bubbleIndex = index; while (bubbleIndex != )
{
// Parent node of the bubble node
int parentIndex = (bubbleIndex-) / D; if (node.CompareTo(heap[parentIndex]) < ) {
// 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[]; numberOfItems--;
if (numberOfItems == ) return returnItem; // Last item in the heap array
var swapItem = heap[numberOfItems]; int swapIndex = , parent; while (true) {
parent = swapIndex;
var curSwapItem = swapItem;
int pd = parent * D + ; // 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 way
if (pd <= numberOfItems)
{ for(int i = ;i<D-;i++)
{
if (pd+i < numberOfItems && (heap[pd+i].CompareTo(curSwapItem) < ))
{
curSwapItem = heap[pd+i];
swapIndex = pd+i;
} } if (pd+D- < numberOfItems && (heap[pd+D-].CompareTo(curSwapItem) < ))
{
swapIndex = pd+D-;
}
} // 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 position
heap[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
欢迎评论区交流,批评,指正~
原创文章,转载请标明出处,谢谢~
d-ary heap实现一个快速的优先级队列(C#)的更多相关文章
- STL之heap与优先级队列Priority Queue详解
一.heap heap并不属于STL容器组件,它分为 max heap 和min heap,在缺省情况下,max-heap是优先队列(priority queue)的底层实现机制.而这个实现机制中的m ...
- GO语言heap剖析及利用heap实现优先级队列
GO语言heap剖析 本节内容 heap使用 heap提供的方法 heap源码剖析 利用heap实现优先级队列 1. heap使用 在go语言的标准库container中,实现了三中数据类型:heap ...
- 一个C优先级队列实现
刚下班没事干,实现了一个简单的优先级队列 #include <stdlib.h>#include <stdio.h> typedef void (*pqueue_setinde ...
- Python之实现一个优先级队列
问题 怎样实现一个按优先级排序的队列? 并且在这个队列上面每次 pop 操作总是返回优先级最高的那个元素 解决方案 下面的类利用 heapq 模块实现了一个简单的优先级队列: import heapq ...
- ACM/ICPC 之 优先级队列+设置IO缓存区(TSH OJ-Schedule(任务调度))
一个裸的优先级队列(最大堆)题,但也有其他普通队列的做法.这道题我做了两天,结果发现是输入输出太过频繁,一直只能A掉55%的数据,其他都是TLE,如果将输入输出的数据放入缓存区,然后满区输出,可以将I ...
- C# 优先级队列
前6行是优先队列,后6行是C#原生的queue Min Heap Priority Queue Works with: C# version 3.0+/DotNet 3.5+ The above co ...
- 【python cookbook】【数据结构与算法】5.实现优先级队列
问题:要实现一个队列,它能够以给定的优先级对元素排序,且每次pop操作时都会返回优先级最高的那个元素: 解决方案:采用heapq模块实现一个简单的优先级队列 # example.py # # Exam ...
- 笔试算法题(57):基于堆的优先级队列实现和性能分析(Priority Queue based on Heap)
议题:基于堆的优先级队列(最大堆实现) 分析: 堆有序(Heap-Ordered):每个节点的键值大于等于该节点的所有孩子节点中的键值(如果有的话),而堆数据结构的所有节点都按照完全有序二叉树 排.当 ...
- [PY3]——实现一个优先级队列
import heapq class PriorityQueue: def __init__(self): self._queue=[] self._index=0 def push(self,ite ...
随机推荐
- ARC与Toll-Free Bridging
arc模块与mrc模块的沟通. 相当于程序的混编处理. Toll-Free Briding保证了在程序中,可以方便和谐的使用Core Foundation类型的对象和Objective-C类型的对象. ...
- 翻新并行程序设计的认知整理版(state of the art parallel)
近几年,业内对并行和并发积累了丰富的经验.有了较深刻的理解.但之前积累的大量教材,在当今的软硬件体系下.反而都成了负面教材.所以,有必要加强宣传,翻新大家的认知. 首先.天地倒悬,结论先行:当你须要并 ...
- virtualbox迁移已建虚机存储磁盘方法
1. 先关闭虚拟机 2. 将虚拟机的磁盘拷贝或移动到想要存储的位置,virtualbox一般为.vdi文件(虚拟磁盘文件) 3. vboxmanage internalcommands sethduu ...
- Java并发编程--7.Java内存操作总结
主内存和工作内存 工作规则 Java内存模型, 定义变量的访问规则, 即将共享变量存储到内存和取出内存的底层细节 所有的变量都存储在主内存中,每条线程有自己的工作内存,工作内存中用到的变量, 是从主 ...
- 最近邻规则分类(k-Nearest Neighbor )机器学习算法python实现
综述 Cover和Hart在1968年提出了最初的近邻算法 是分类(classification)算法 输入基于实例的学习(instance-based learning),惰性学习(lazy lea ...
- RegExp exec有记忆性的问题
当 RegExpObject 是作为一个变量时时.每次调用完exec()后.它会在 RegExpObject 的 lastIndex 属性指定的字符处开始检索字符串 string.当 exec() 找 ...
- 【VCS】种草VSCode
VSCode官网 特点:更轻量.集成Git.快速diff:文件目录管理,自定义配置,插件库 种草推文 下载地址 下载安装VSCode之后按照种草推文指南 配置VSC的扩展开发(有个前提:注册了Micr ...
- MariaDB中文乱码之解决思路
首先出现乱码的原因就是编码不一致问题引起的,那么就从以下2个方面入手: 1.应用层:前提条件数据库服务端存储的中文数据是对的,但是页面上显示乱码,这里只需要检查你的项目的编码格式,设置成一致就行. 2 ...
- Mysql5.7登录错误1045和1130的解决方法,亲测有用,希望能帮助到你们。
Mysql (针对Mysql5.7版本,其他版本可能略有不同) 错误:1045 解决方法: 以管理员身份运行cmd(win8系统:win+x 键 ,再按 A键 ),进入Mysql安装目录下的bin目录 ...
- [COGS257]动态排名系统 树状数组套主席树
257. 动态排名系统 时间限制:5 s 内存限制:512 MB [问题描述]给定一个长度为N的已知序列A[i](1<=i<=N),要求维护这个序列,能够支持以下两种操作:1.查询A[ ...