前言
最近在刷牛客时发现,牛客的C#的.net版本有些低,竟然不能使用官方的优先队列(PriorityQueue),导致想要使用优先队列去做题的话,那只能换一个支持优先队列的语言,例如java或c++。但是对于我这种比较习惯使用C#,java和c++不熟练的人来说,在语法和库上又会有很多难受的地方,所以我决定自己实现一个优先队列,这样方便我自己使用。
优先队列原理
什么是堆
优先队列一般是使用堆区实现的,堆这个数据结构一般是使用数组去实现的。堆这个数据结构本质上是一个完全二叉树,但是如果按照树的方式去创建,那么指针方面去处理结点会有些许不便,所以大部分情况都会选择使用数组实现。
堆的分类
对于堆的分类一般有两种,一种是大根堆,一种是小根堆。大根堆就是对于每个父节点,他一定比他的孩子节点要大。小根堆就是相反,一定比他的孩子要小。
原理
优先队列实机上就是按照你自己定义的优秀级去觉得大根堆或小根堆和比较方式,然后每次即可从队顶获得你想要的最高或最低优先级的节点。而堆的插入和删除操作时间复杂度均为O(log(n)),这样对于一个经常要进行插入和删除操作,且要维护数据的有序性时,优先队列将是一个非常好的数据结构。
优先队列实现
底层数据结构我选择使用数组去实现,因为数组在对节点进行移动操作时会方便很多。官方的优先队列是将最小优先级作为堆顶,所以在这我也和官方一样,然后选择使用小根堆实现。
定义成员变量
private T[] items; //堆
private Comparison<T> comparison; //优先队列比较器
private int count; //数据数量
public int Count { get => count; } //数据数量属性,防止外部修改
public int Capacity { get => items == null ? 0 : items.Length; } //堆的当前容量
提示:再C#中,比较器一般是有两种实现方式,一个是通过实现ICompare接口,实现其中的CompareTo方法,然后把实现类传进去。一种是通过C#的语言特性,使用委托方式去实现,委托中官方常用的就是Comparison。C#中大量使用比较器的类一般都提供两种方式,但是再官方的PriorityQueue中只提供了第一种方式。个人在使用这些比较器是习惯使用第二种,因为他方便,可以之间通过lamda表达式(匿名方法)去传参,不需要自己专门写个类如此麻烦。所以这里我选择使用委托实现。
初始化操作
默认容量选择定义为10,默认比较器选择通过hashCode比较。
public MyPriortyQueue()
{
items = new T[10];
comparison = (x, y) => x.GetHashCode().CompareTo(y.GetHashCode());
}
public MyPriortyQueue(Comparison<T> comparison) : this()
{
this.comparison = comparison;
}
根据在C#中的Sort排序一般都有默认的比较器,所以这里我也定义了一个无参构造时的默认比较器,但是Sort中的默认比较器是按什么进行比较的我没有查到,所以我选择使用hashCode作用默认比较方式。
带参构造中,首先要构造无参构造的初始化方式,然后再更新他的比较器。
具体实现
做完以上初始化后就到具体的实现了。
数组扩容
考虑到我选择使用的是数组,数组大小只能在定义时决定,无法动态变化,所以再节点不断加入时会出现数组容量不够的情况,这时就需要做数组扩容操作。
private void Expansion()
{
T[] newItems = new T[Capacity * 2];
for (int i = 0; i < count; i++)
newItems[i] = items[i];
items = newItems;
}
扩容操作很简单,就是创建一个更大容量的数组,然后将原数组数据迁移过去即可。扩容一般都是按照当前大小两边扩容。
当然容量可能会出现浪费,官方中经常也会有缩小空间的操作,这里我也没有过多研究,所以没有做实现。有兴趣的可以去看看官方的容器的源码,官方的容器大部分底层都是通过数组实现的,一般都有这些操作,可以去参考。
交换数组数据
由于要频繁做数组中内容交换,所以写了一个交换方法。
private void Swap(T[] array, int i, int j)
{
T t = array[i];
array[i] = array[j];
array[j] = t;
}
Heapify
首先先实现堆最重要的操作Heapify,Heapify是堆常用的一个操作,他的意义是在父节点和他的左右中选择一个最大(或最小)的结点作为新的父节点,原理的父节点要和旧父节点做位置交换,然后再从旧父节点再往下做相同的操作,知道新父节点和旧父节点一样时停止(即当前父节点就是三个节点中最大或最小的节点)。
private void Heapify(int node)
{
int lc = 2 * node + 1;
int rc = 2 * node + 2;
int min = node;
if (lc < count && comparison(items[min], items[lc]) > 0)
min = lc;
if (rc < count && comparison(items[min], items[rc]) > 0)
min = rc;
if (min != node)
{
Swap(items, node, min);
Heapify(min);
}
}
由于我选择使用的是小根堆,所以再比较时,我只选择出最小节点。
提示:在做heapify操作时,由于数组容量可能会比实际数据要更多,所以要注意左右孩子是否会越界。
进队
进队时优先队列的重要方法,在每个元素进队时,都要保持小根堆不会出错。这里可以去查看小根堆的插入操作实现方式。
public void Enqueue(T item)
{
if (count >= Capacity)
Expansion();
items[count] = item;
int cur = count++;
if (cur == 0)
return;
int parent = cur;
T oldValue;
T newValue;
do
{
cur = parent;
parent = (cur - 1) / 2;
oldValue = items[parent];
Heapify(parent);
newValue = items[parent];
} while (!oldValue.Equals(newValue));
}
进队时,首先先确定容量是否充足,决定是否扩容。然后就是将新节点之间放入队尾,在从队尾的父节点开始进行heapify操作,操作完后查看当前heapify是否成功(发生节点交换即为成功,否则为失败),成功需要继续向父节点的父节点进行heapify操作,直到heapify操作失败。然后记得让Count加一。
出队
出队是将第一个节点移出队列,即将最小优先级的移出。并且保持当前数据仍未小根堆。实现方式可以查看小根堆的移除操作。
public T Dequeue()
{
if (count == 0)
throw new Exception("the queue is empty");
T result = items[0];
items[0] = default(T);
Swap(items, 0, count - 1);
count--;
if (count > 0)
Heapify(0);
return result;
}
出队时,先判断队内是否有元素,没有则抛出异常,有则记录堆顶元素,将堆顶元素和队尾进行交换,并减小Count,对堆顶进行一次heapify操作(在元素只有一个时可以不做heapify)。
提示:在删除时记得将第一个元素设置回默认值,不然如果T是引用类型,会导致无法被垃圾回收,引起内存泄漏。
查看队顶元素
这个实现很简单,之间取队顶元素返回即可。
public T Peek()
{
if (count == 0)
throw new Exception("the queue is empty");
return items[0];
}
这样就实现了一个优先队列。
测试
这里我选择了leetcode的困难题,剑指 Offer II 078. 合并排序链表
首先是官方的优先队列实现。
public class Solution {
public ListNode MergeKLists(ListNode[] lists) {
PriorityQueue<ListNode, int> queue = new ();
foreach (var item in lists)
if (item != null)
queue.Enqueue(item, item.val);
ListNode root = new ();
ListNode cur = root;
while (queue.Count > 0)
{
ListNode node = queue.Dequeue();
cur.next = node;
cur = cur.next;
if (node.next != null)
queue.Enqueue(node.next, node.next.val);
}
return root.next;
}
}
然后是自己的优先队列实现
public class Solution {
public ListNode MergeKLists(ListNode[] lists) {
MyPriortyQueue<ListNode> queue = new MyPriortyQueue<ListNode>((x, y) => x.val.CompareTo(y.val));
foreach (var item in lists)
if (item != null)
queue.Enqueue(item);
ListNode root = new ();
ListNode cur = root;
while (queue.Count > 0)
{
ListNode node = queue.Dequeue();
cur.next = node;
cur = cur.next;
if (node.next != null)
queue.Enqueue(node.next);
}
return root.next;
}
public class MyPriortyQueue<T>
{
private T[] items;
private Comparison<T> comparison;
private int count;
public int Count { get => count; }
public int Capacity { get => items == null ? 0 : items.Length; }
public MyPriortyQueue()
{
items = new T[10];
comparison = (x, y) => x.GetHashCode().CompareTo(y.GetHashCode());
}
public MyPriortyQueue(Comparison<T> comparison) : this()
{
this.comparison = comparison;
}
public void Enqueue(T item)
{
if (count >= Capacity)
Expansion();
items[count] = item;
int cur = count++;
if (cur == 0)
return;
int parent = cur;
T oldValue;
T newValue;
do
{
cur = parent;
parent = (cur - 1) / 2;
oldValue = items[parent];
Heapify(parent);
newValue = items[parent];
} while (!oldValue.Equals(newValue));
}
public T Dequeue()
{
if (count == 0)
throw new Exception("the queue is empty");
T result = items[0];
items[0] = default(T);
Swap(items, 0, count - 1);
count--;
if (count > 0)
Heapify(0);
return result;
}
public T Peek()
{
if (count == 0)
throw new Exception("the queue is empty");
return items[0];
}
private void Heapify(int node)
{
int lc = 2 * node + 1;
int rc = 2 * node + 2;
int min = node;
if (lc < count && comparison(items[min], items[lc]) > 0)
min = lc;
if (rc < count && comparison(items[min], items[rc]) > 0)
min = rc;
if (min != node)
{
Swap(items, node, min);
Heapify(min);
}
}
private void Swap(T[] array, int i, int j)
{
T t = array[i];
array[i] = array[j];
array[j] = t;
}
private void Expansion()
{
T[] newItems = new T[Capacity * 2];
for (int i = 0; i < count; i++)
newItems[i] = items[i];
items = newItems;
}
}
}
结果相差不大,所以成功实现。