优先队列(c#)
真实场景
现实生活中存在按一定优先级处理一组事务,对象的场景:
- 按照排队先后顺序结账收银,越早加入排队的人优先级越高,越早结账;
- 按照接收的先后顺序处理邮件,最新收到的邮件优先处理;
- 对医院、学校、餐馆进行综合打分(综合考虑收费,技术,服务,环境等),分数高的被优先选择;
- ...
抽象
为获得最大的通用性,我们对上述各特异的场景进行抽象,会发现存在以下几个要素和动作。
要素
- 一组需要处理的事务
- 一种优先级规则,如时间顺序,综合打分
动作
- 添加新事务
- 移除最高优先事务
- 了解待处理事务的规模
API
我们将上述要素和动作封装成一种数据结构————优先队列,其api如下:
api | description |
---|---|
MaxPQ() | 无参构造函数 |
MaxPQ(IComparer<T> comparer) | 构造函数,参数为优先级比较器 |
Push(T item) | 添加对象 |
T Pop() | 取出最高优先级对象 |
int Size | 获取当前待处理事务的规模 |
bool IsEmpty() | 当前对象数量是否为0 |
实现
思路
内部维护对象的集合(可以是有序链表,有序数组,无序数组,堆有序的二叉堆),通过一个int记录集合的规模,从而可以在常数时间内获取对象集合的规模。
内部对象集合实现的不同,会对实现思路,执行效率产生巨大影响。
集合类型 | 插入 | 取出 | 效率 |
---|---|---|---|
无序数组 | 插入到数组的末尾 | 对数组进行排序,取出最大的 | 插入效率:1取出效率:N |
有序数组 | 添加到数组末端,重新有序化(插入排序) | 取出最大的 | 插入效率:N 取出效率:1 |
链表 | 添加到链表末端,重新有序化(插入到合适位置) | 取出最大的 | 插入效率:N 取出效率:1 |
堆有序的堆 | 添加到堆底,重新有序化(上浮) | 取出堆顶,将堆底移动到堆顶,重新有序化(下沉) | log N |
using System;
using System.Collections.Generic;
namespace LearnAL.DataStructs.Generic
{
public class MaxPQ<T>
{
private T[] _heap;
private int _size;
private IComparer<T> _cmper;
public MaxPQ() : this(null) { }
/// <summary>
/// 定义某种优先级规则下的优先队列
/// </summary>
/// <param name="cmper">优先级比较器</param>
public MaxPQ(IComparer<T> cmper)
{
_heap = new T[2];
_heap[0] = default;
_cmper = cmper == null ? Comparer<T>.Default : cmper;
}
public (bool done, T value) Pop()
{
if (IsEmpty())
{
return (false, default);
}
var result = _heap[1];
Exchange(1, _size);
_size--;
Sink(1);
if ((_size + 1) <= (_heap.Length / 4))
{
Resize(ref _heap, _heap.Length / 2);
}
return (true, result);
}
public void Push(T item)
{
if (_size == _heap.Length - 1)
{
Resize(ref _heap, _heap.Length * 2);
}
_size++;
_heap[_size] = item;
Swim(_size);
}
public bool IsEmpty()
{
return _size == 0;
}
#region 辅助函数
private void Swim(int downIdx)
{
while (downIdx >= 2 && downIdx <= _size)
{
var upIdx = downIdx / 2;
var upValue = _heap[upIdx];
var downValue = _heap[downIdx];
if (_cmper.Compare(upValue, downValue) <= 0)
{
Exchange(upIdx, downIdx);
downIdx = upIdx;
}
else
{
break;
}
}
}
private void Sink(int upIdx)
{
while (upIdx * 2 <= _size)
{
var leftDownIdx = upIdx * 2;
var rightDownIdx = upIdx * 2 + 1;
var downIdx = MaxSide(_heap, leftDownIdx, rightDownIdx);
var up = _heap[upIdx];
var down = _heap[downIdx];
if (_cmper.Compare(up, down) <= 0)
{
Exchange(upIdx, downIdx);
upIdx = downIdx;
}
else
{
break;
}
}
}
private void Resize(ref T[] heap, int size)
{
Array.Resize(ref _heap, size);
}
private void Exchange(int idx1, int idx2)
{
var temp = _heap[idx1];
_heap[idx1] = _heap[idx2];
_heap[idx2] = temp;
}
private int MaxSide(IList<T> heap, int leftDownIdx, int rightDownIdx)
{
if (rightDownIdx > _size)
{
return leftDownIdx;
}
else
{
var left = heap[leftDownIdx];
var right = heap[rightDownIdx];
var result = _cmper.Compare(left, right) >= 0 ? leftDownIdx : rightDownIdx;
return result;
}
}
}
#endregion
}
测试
using System;
using Xunit;
using LearnAL.DataStructs.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace LearnAL.Tests
{
public class MaxPQTests
{
[Fact]
public void Test()
{
//测试只有一个对象的时候的正确性
var pq0 = new MaxPQ<Person>(new PersonCompaer());
pq0.Push(new Person(1));
Assert.True(pq0.Pop().value.Age == 1);
var pq = new MaxPQ<Person>(new PersonCompaer()); ;
var values = new List<int>();
values.Add(1);
values.Add(9999999);
values.Add(8888888);
values.Add(7777777);
var middleValueCount = 996;
for (int i = 0; i < middleValueCount; i++)
{
var rand = new Random();
var value = rand.Next(2, 999999);
values.Add(value);
}
for (int i = 0; i < values.Count; i++)
{
var person = new Person(values[i]);
pq.Push(person);
}
Assert.True(pq.Pop().value.Age == 9999999);
Assert.True(pq.Pop().value.Age == 8888888);
Assert.True(pq.Pop().value.Age == 7777777);
for (int i = 0; i < values.Count - 4; i++)
{
pq.Pop();
}
Assert.True(pq.Pop().value.Age == 1);
}
public class Person
{
public Person(int age)
{
this.Age = age;
}
public int Age { get; set; }
}
public class PersonCompaer : Comparer<Person>
{
public override int Compare([AllowNull] Person x, [AllowNull] Person y)
{
return x.Age.CompareTo(y.Age);
}
}
}
}