前言

前段时间看到有大佬对.net 6.0新出的PriorityQueue(优先级队列)数据结构做了解析,但是没有源码分析,所以本着探究源码的心态,看了看并分享出来。它不像普通队列先进先出(FIFO),而是根据优先级出队。

ps:读者多注意代码的注释。

D叉树的认识(d-ary heap)

首先我们在表示一个堆(大顶堆或小顶堆)的时候,实际上是通过一个一维数组来维护一个二叉树(d=2,d表示每个父节点最多有几个子节点),首先看下图的二叉树,数字代表索引:

  • 任意一个节点的父节点的索引为:(index - 1) / d
  • 任意一个节点的左子节点的索引为:(index * d) + 1
  • 任意一个节点的右子节点的索引为:(index * d) + 2
  • 它的时间复杂度为O(logndn)

    通过以上公式,我们就可以轻松通过一个数组来表达一个堆,只需保证能拿到正确的索引即可进行快速的插入和删除。

源码解析

构造初始化

关于这部分主要介绍关键的字段和方法,比较器的初始化以及堆的初始化,请看如下代码:

public class PriorityQueue<TElement, TPriority>
{
/// <summary>
/// 保存所有节点的一维数组且每一项是个元组
/// </summary>
private (TElement Element, TPriority Priority)[] _nodes; /// <summary>
/// 优先级比较器,这里用的泛型,比较器可以自己实现
/// </summary>
private readonly IComparer<TPriority>? _comparer; /// <summary>
/// 当前堆的大小
/// </summary>
private int _size; /// <summary>
/// 版本号
/// </summary>
private int _version; /// <summary>
/// 代表父节点最多有4个子节点,也就是d=4(d=4时好像效率最高)
/// </summary>
private const int Arity = 4; /// <summary>
/// 使用位运算符,表示左移2或右移2(效率更高),即相当于除以4,
/// </summary>
private const int Log2Arity = 2; /// <summary>
/// 构造函数初始化堆和比较器
/// </summary>
public PriorityQueue()
{
_nodes = Array.Empty<(TElement, TPriority)>();
_comparer = InitializeComparer(null);
} /// <summary>
/// 重载构造函数,来定义比较器否则使用默认的比较器
/// </param>
public PriorityQueue(IComparer<TPriority>? comparer)
{
_nodes = Array.Empty<(TElement, TPriority)>();
_comparer = InitializeComparer(comparer);
}
private static IComparer<TPriority>? InitializeComparer(IComparer<TPriority>? comparer)
{
//如果是值类型,如果是默认比较器则返回null
if (typeof(TPriority).IsValueType)
{
if (comparer == Comparer<TPriority>.Default)
{
return null;
} return comparer;
}
//否则就使用自定义的比较器
else
{
return comparer ?? Comparer<TPriority>.Default;
}
} /// <summary>
/// 获取索引的父节点
/// </summary>
private int GetParentIndex(int index) => (index - 1) >> Log2Arity; /// <summary>
/// 获取索引的左子节点
/// </summary>
private int GetFirstChildIndex(int index) => (index << Log2Arity) + 1;
}

单元总结:

  1. 实际所有元素使用一维数组来维护这个堆。
  2. 调用方可以自定义比较器,但是类型得一致。 如果没有比较器,则使用默认的比较器。
  3. 默认一个父节点最多有4个子节点,D=4时效率好像是最好的。
  4. 获取父节点索引位置和子节点索引位置使用位运算符,效率更高。

入队操作

入队操作操作相对简单,主要是做扩容和插入处理,请看如下代码:

public void Enqueue(TElement element, TPriority priority)
{
//拿到最大位置的索引,然后再将数组长度+1
int currentSize = _size++;
_version++;
//如果长度相等,说明已经到达最大位置,数组需要扩容了才能容下更多的元素
if (_nodes.Length == currentSize)
{
//扩容,参数是代表数组最小容量
Grow(currentSize + 1);
} if (_comparer == null)
{ MoveUpDefaultComparer((element, priority), currentSize);
}
else
{
MoveUpCustomComparer((element, priority), currentSize);
}
}
private void Grow(int minCapacity)
{
//增长倍数
const int GrowFactor = 2;
//每次扩容的最小值
const int MinimumGrow = 4;
//每次扩容都2倍扩容
int newcapacity = GrowFactor * _nodes.Length; //数组不能大于最大长度
if ((uint)newcapacity > Array.MaxLength) newcapacity = Array.MaxLength; //使用他们两个的最大值
newcapacity = Math.Max(newcapacity, _nodes.Length + MinimumGrow); //如果比参数小,则使用参数的最小值
if (newcapacity < minCapacity) newcapacity = minCapacity;
//重新分配内存,设置大小,因为数组的保存在内存中是连续的
Array.Resize(ref _nodes, newcapacity);
}
private void MoveUpDefaultComparer((TElement Element, TPriority Priority) node, int nodeIndex)
{
//nodes保存副本
(TElement Element, TPriority Priority)[] nodes = _nodes;
//这里入队操作是从空闲索引第一个位置开始插入
while (nodeIndex > 0)
{
//找父节点索引位置
int parentIndex = GetParentIndex(nodeIndex);
(TElement Element, TPriority Priority) parent = nodes[parentIndex];
//插入节点和父节点比较,如果小于0(默认比较器情况下构建的小顶堆),则交换位置
if (Comparer<TPriority>.Default.Compare(node.Priority, parent.Priority) < 0)
{
nodes[nodeIndex] = parent;
nodeIndex = parentIndex;
}
//算是性能优化吧,不必检查所有节点,当发现大于时,就直接退出就可以了
else
{
break;
}
}
//将插入节点放到它应该的位置
nodes[nodeIndex] = node;
}

单元总结:

  1. 首先记录当前元素最大的索引位置,根据适当的情况来扩容。
  2. 扩容正常情况下是以2倍的增长速度扩容。
  3. 插入数据时,从最后一个节点的父节点向上还是找,比较元素的Priority,交换位置,默认情况下是构建小顶堆。

出队操作

出队操作简单来说就是将根元素返回并移除(也就是数组的第一个元素),然后根据比较器将最小或最大的元素放到堆顶,请看如下代码:

public TElement Dequeue()
{
if (_size == 0)
{
throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue);
}
//将堆顶元素返回
TElement element = _nodes[0].Element;
//然后调整堆
RemoveRootNode();
return element;
}
private void RemoveRootNode()
{
//记录最后一个元素的索引位置,并且将堆的大小-1
int lastNodeIndex = --_size;
_version++; if (lastNodeIndex > 0)
{
//堆的大小已经被减1,所以将最后一个元素作为副本,开始从堆顶重新整理堆
(TElement Element, TPriority Priority) lastNode = _nodes[lastNodeIndex];
if (_comparer == null)
{
MoveDownDefaultComparer(lastNode, 0);
}
else
{
MoveDownCustomComparer(lastNode, 0);
}
} if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>())
{
//将最后一个位置的元素设置为默认值
_nodes[lastNodeIndex] = default;
}
}
private void MoveDownDefaultComparer((TElement Element, TPriority Priority) node, int nodeIndex)
{
(TElement Element, TPriority Priority)[] nodes = _nodes;
//堆的实际大小
int size = _size; int i;
//当左子节点的索引小于堆的实际大小时
while ((i = GetFirstChildIndex(nodeIndex)) < size)
{
//左子节点元素
(TElement Element, TPriority Priority) minChild = nodes[i];
//当前左子节点的索引
int minChildIndex = i;
//这里即找到所有同一个父节点的相邻子节点,但是要判断是否超出了总的大小
int childIndexUpperBound = Math.Min(i + Arity, size);
//按照索引区间顺序查找,并根据比较器找到最小的子元素位置
while (++i < childIndexUpperBound)
{
(TElement Element, TPriority Priority) nextChild = nodes[i];
if (Comparer<TPriority>.Default.Compare(nextChild.Priority, minChild.Priority) < 0)
{
minChild = nextChild;
minChildIndex = i;
}
}
//如果最后一个节点的元素,比这个最小的元素还小,那么直接放到父节点即可
if (Comparer<TPriority>.Default.Compare(node.Priority, minChild.Priority) <= 0)
{
break;
}
//将最小的子元素赋值给父节点
nodes[nodeIndex] = minChild;
nodeIndex = minChildIndex;
}
//将最后一个节点放到空闲出来的索引位置
nodes[nodeIndex] = node;
}

单元总结:

  1. 返回根节点元素,然后移除根节点元素,调整堆。
  2. 从根节点开始,依次查找对应父节点的所有子节点,放到堆顶,也就是数组索引0的位置,然后如果树还有层数,继续循环查找。
  3. 将最后一个元素放到堆适当的位置,然后再将最后一个位置的元素置为默认值。

总结

通过源码的解读,除了了解类的设计之外,更对对优先级队列数据结构的实现有了更加深刻和清晰的认识。

这里也只是粘贴出主要的代码,需要看详细代码的请点击这里,笔者可能有一些解读错误的地方,欢迎评论指正。

源码解析C#中PriorityQueue(优先级队列)的实现的更多相关文章

  1. 源码解析.Net中IConfiguration配置的实现

    前言 关于IConfituration的使用,我觉得大部分人都已经比较熟悉了,如果不熟悉的可以看这里.因为本篇不准备讲IConfiguration都是怎么使用的,但是在源码部分的解读,网上资源相对少一 ...

  2. 源码解析.Net中Host主机的构建过程

    前言 本篇文章着重讲一下在.Net中Host主机的构建过程,依旧延续之前文章的思路,着重讲解其源码,如果有不知道有哪些用法的同学可以点击这里,废话不多说,咱们直接进入正题 Host构建过程 下图是我自 ...

  3. Spark 源码解析 : DAGScheduler中的DAG划分与提交

    一.Spark 运行架构 Spark 运行架构如下图: 各个RDD之间存在着依赖关系,这些依赖关系形成有向无环图DAG,DAGScheduler对这些依赖关系形成的DAG,进行Stage划分,划分的规 ...

  4. 源码解析.Net中DependencyInjection的实现

    前言 笔者的这篇文章和上篇文章思路一样,不注重依赖注入的使用方法,更加注重源码的实现,我尽量的表达清楚内容,让读者能够真正的学到东西.如果有不太清楚依赖注入是什么或怎么在.Net项目中使用的话,请点击 ...

  5. 源码解析.Net中Middleware的实现

    前言 本篇继续之前的思路,不注重用法,如果还不知道有哪些用法的小伙伴,可以点击这里,微软文档说的很详细,在阅读本篇文章前,还是希望你对中间件有大致的了解,这样你读起来可能更加能够意会到意思.废话不多说 ...

  6. multiprocessing 源码解析 更新中......

    一.参考链接 1.源码包下载·链接:   https://pypi.org/search/?q=multiprocessing+ 2.源码包 链接:https://pan.baidu.com/s/1j ...

  7. java中PriorityQueue优先级队列使用方法

    优先级队列是不同于先进先出队列的另一种队列.每次从队列中取出的是具有最高优先权的元素. PriorityQueue是从JDK1.5开始提供的新的数据结构接口. 如果不提供Comparator的话,优先 ...

  8. 源码解析Android中View的measure量算过程

    Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算.布局.绘图的总体机制可参见博文< Android中View ...

  9. 《转》JAVA中PriorityQueue优先级队列使用方法

    该文章转自:http://blog.csdn.net/hiphopmattshi/article/details/7334487 优先级队列是不同于先进先出队列的另一种队列.每次从队列中取出的是具有最 ...

随机推荐

  1. Shell 打印文件的最后5行

    目录 Shell 打印文件的最后5行 题解-awk 题解-tail Shell 打印文件的最后5行 经常查看日志的时候,会从文件的末尾往前查看,于是请你写一个 bash脚本以输出一个文本文件 nowc ...

  2. jQuery无限载入瀑布流 【转载】

    转载至 http://wuyuans.com/2013/08/jquery-masonry-infinite-scroll/ jQuery无限载入瀑布流 好久没更新日志了,一来我比较懒,二来最近也比较 ...

  3. 从jvm字节码指令看i=i++和i=++i的区别

    1. 场景的产生 先来看下下面代码展示的两个场景 @Testvoid testIPP() { int i = 0; for (int j = 0; j < 10; j++) { i = i++; ...

  4. oracle 以SYSDBA远程连接数据库

    在服务器用sysdba登陆 grant sysdba to system 然后在远程就可以sysdba登陆数据库了

  5. 【编程思想】【设计模式】【结构模式Structural】装饰模式decorator

    Python版 https://github.com/faif/python-patterns/blob/master/structural/decorator.py #!/usr/bin/env p ...

  6. SpringBoot环境下java实现文件的下载

    思路:文件下载,就是给服务器上的文件创建输入流,客户端创建输出流,将文件读出,读入到客户端的输出流中,(流与流的转换) package com.cst.icode.controller; import ...

  7. 访问者模式(Visitor Pattern)——操作复杂对象结构

    模式概述 在软件开发中,可能会遇到操作复杂对象结构的场景,在该对象结构中存储了多个不同类型的对象信息,而且对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式,还有可能增加新的处理 ...

  8. py脚本 获取当前运行服务的相关信息

    一.简介 最近在统计系统中都部署了什么服务,但服务器太多,在没有标准化之前进行整理,还是写脚本收集方便一些. 当然还是需要人工去判断整理表格,为后面标准化做准备.脚本是python2.7的,默认的ce ...

  9. 使用 Nocalhost 开发 Kubernetes 中的 APISIX Ingress Controller

    本文作者:黄鑫鑫 - Nocalhost 项目核心开发者 腾讯云 CODING DevOps 研发工程师.硕士毕业于中山大学数据科学与计算机学院,曾负责过平安云主机及国家超算中心容器云平台等相关业务, ...

  10. CF102B Sum of Digits 题解

    Content 给定一个数 \(n\),每次操作可以将 \(n\) 变成 \(n\) 各位数之和.问你几次操作之后可以将 \(n\) 变为一位数. 数据范围:\(1\leqslant n\leqsla ...