您的位置:首页 > 产品设计 > UI/UE

C# Queue源码剖析

2016-05-02 13:13 405 查看
源代码版本为 .NET Framework 4.6.1

本系列持续更新,敬请关注

有投入,有产出。

(注:非基础性,主要涉及Queue的实现原理)

水平有限,若有不对之处,望指正。

Queue表示对象的先进先出集合。实现了ICollection接口,可以由数组或链表两种形式实现,在.NET中是以数组的形式实现的。

概念

队列是一种特殊的线性表,特殊之处在于它只允许在表头(head)进行删除操作,而在表尾(tail)进行插入操作。



队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素成为出队。因为队列只允许在一段插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表

队列可以分为顺序队列和循环队列,.NET中为了提高空间的利用率,采用的是循环队列。

循环队列

为充分利用向量空间,克服”假溢出”(由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用)现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。概念图如下:



循环队列中,由于入队时尾指针向前追赶头指针;出队时头指针向前追赶尾指针,造成空队列和满队列时头尾指针均相等。因此,无法通过条件front==rear来判别队列是”空”还是”满”,.NET使用一下方法判断空队列和满队列(实际.NET中,队列的长度时自动扩容的):

(1)私有成员_size = 0时,为空队列。

(2)_size == _array.Length时(_array为Queue内部维护的实际数据数组),为满队列,这个时候会进行自动扩容(新建一个2倍于原容量的数组)。

进入主题,上代码解说

基本成员

private T[] _array;
private int _head;       // 表头
private int _tail;       // 表尾
private int _size;       // 队列元素数量
private int _version;
[NonSerialized]
private Object _syncRoot;

private const int _MinimumGrow = 4;        // 最小增长值
private const int _ShrinkThreshold = 32;   // 收缩阈值
private const int _GrowFactor = 200;       // 每次增长双倍
private const int _DefaultCapacity = 4;    // 默认容量
static T[]  _emptyArray = new T[0];        //空数组


_head:一个指向队列头元素的索引,因为是循环列队,_head值有可能大于_tail值。

_tail:一个指向队列尾元素的索引,因为是循环列队,_tail值有可能笑于_head值。

_size:记录队列中元素的总数量。

_MinimumGrow: 队列每一次的最小扩容量。

_ShrinkThreshold:“迷”之收缩阈值。

_GrowFactor:增长因子。

_DefaultCapacity: 默认初始容量。

_emptyArray:默认空数组。

初始化函数

public Queue() {
_array = _emptyArray;
}

public Queue(int capacity) {
if (capacity < 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNumRequired);

_array = new T[capacity];
_head = 0;
_tail = 0;
_size = 0;
}

public Queue(IEnumerable<T> collection){
if (collection == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);

_array = new T[_DefaultCapacity];
_size = 0;
_version = 0;

using(IEnumerator<T> en = collection.GetEnumerator()) {
while(en.MoveNext()) {
Enqueue(en.Current);
}
}
}


有三个初始化函数,一般初始化都会使用初始默认值_head和_tail最初都指向索引0(不代表_head=_tail就是空数组),_size=0长度为0。

入队

突然想起,军训期间被罚做完俯卧撑后,教官大喊入队。

入队的方法:Enqueue(T item)

public void Enqueue(T item) {
//动态扩容
if (_size == _array.Length) {
int newcapacity = (int)((long)_array.Length * (long)_GrowFactor / 100);
if (newcapacity < _array.Length + _MinimumGrow) {
newcapacity = _array.Length + _MinimumGrow;
}
SetCapacity(newcapacity);
}

_array[_tail] = item;
_tail = (_tail + 1) % _array.Length;
_size++;
_version++;
}


入队看起来很简单,通过_tail拿到队尾索引,将元素插入队尾即可。但是这里有一点小细节:

_tail = (_tail + 1) % _array.Length


下个元素的队尾索引 = (当前队尾索引 + 1) % 数据数组长度,这里的数据数组长度不等同与队列元素数量。此公式与抑制了队尾索引不会超过数据数组长度,从而避免了数据溢出的产生(同时也会导致队尾索引会小于队头索引,需要分情况进行处理)。

出队

出队有两种方法

(1)public T Peek():返回位于 Queue 开始处的对象但不将其移除。

(2)public T Dequeue():移除并返回位于 Queue 开始处的对象。

public T Peek() {
if (_size == 0)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyQueue);

return _array[_head];
}

public T Dequeue() {
if (_size == 0)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyQueue);

T removed = _array[_head];
_array[_head] = default(T);
_head = (_head + 1) % _array.Length;
_size--;
_version++;
return removed;
}


Peek()简单粗暴,通过索引直接返回数据。

Queue没有提供Remove方法,但是Dequeue具有删除功能并返回头元素。被移除的元素直接指向null(空引用)。并且头元素索引向前移动(这是一个跑圈圈的游戏,你懂的)。

查询

Contains(T item)判断队列中是否至少包含一个匹配的元素存在 是则返回true,否则返回false。

public bool Contains(T item) {
int index = _head;
int count = _size;

EqualityComparer<T> c = EqualityComparer<T>.Default;
while (count-- > 0) {
if (((Object) item) == null) {
if (((Object) _array[index]) == null)
return true;
}
else if (_array[index] != null && c.Equals(_array[index], item)) {
return true;
}
index = (index + 1) % _array.Length;
}

return false;
}


容量调整

容量调整,可以重置队列空间,如果元素数小于当前容量的 90%,将容量设置为 Queue中的实际元素数。

public void TrimExcess() {
int threshold = (int)(((double)_array.Length) * 0.9);
if( _size < threshold ) {
SetCapacity(_size);
}
}


private void SetCapacity(int capacity) {
//创建新数组
T[] newarray = new T[capacity];
if (_size > 0) {
if (_head < _tail) {
//头索引小于尾索引
Array.Copy(_array, _head, newarray, 0, _size);
} else {
//头索引大于尾索引
Array.Copy(_array, _head, newarray, 0, _array.Length - _head);
Array.Copy(_array, 0, newarray, _array.Length - _head, _tail);
}
}

_array = newarray;
_head = 0;
_tail = (_size == capacity) ? 0 : _size;
_version++;
}


最后

(1)Queue可以通过TrimExcess()方法,将容量下降到实际元素的数量.

(2)Queue允许重复的元素。

(3)Queue和Stack主要是用来存储临时信息的。(下篇文章写Stack,一种后进先出的集合)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  .net 源码 线性表 Queue