C# Queue源码剖析
2016-05-02 13:13
405 查看
源代码版本为 .NET Framework 4.6.1
(注:非基础性,主要涉及Queue的实现原理)
水平有限,若有不对之处,望指正。
Queue表示对象的先进先出集合。实现了ICollection接口,可以由数组或链表两种形式实现,在.NET中是以数组的形式实现的。
概念
队列是一种特殊的线性表,特殊之处在于它只允许在表头(head)进行删除操作,而在表尾(tail)进行插入操作。
![](http://img.blog.csdn.net/20160429143922229)
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素成为出队。因为队列只允许在一段插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表
队列可以分为顺序队列和循环队列,.NET中为了提高空间的利用率,采用的是循环队列。
循环队列
为充分利用向量空间,克服”假溢出”(由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用)现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。概念图如下:
![](http://img.blog.csdn.net/20160429111903570)
循环队列中,由于入队时尾指针向前追赶头指针;出队时头指针向前追赶尾指针,造成空队列和满队列时头尾指针均相等。因此,无法通过条件front==rear来判别队列是”空”还是”满”,.NET使用一下方法判断空队列和满队列(实际.NET中,队列的长度时自动扩容的):
(1)私有成员_size = 0时,为空队列。
(2)_size == _array.Length时(_array为Queue内部维护的实际数据数组),为满队列,这个时候会进行自动扩容(新建一个2倍于原容量的数组)。
_head:一个指向队列头元素的索引,因为是循环列队,_head值有可能大于_tail值。
_tail:一个指向队列尾元素的索引,因为是循环列队,_tail值有可能笑于_head值。
_size:记录队列中元素的总数量。
_MinimumGrow: 队列每一次的最小扩容量。
_ShrinkThreshold:“迷”之收缩阈值。
_GrowFactor:增长因子。
_DefaultCapacity: 默认初始容量。
_emptyArray:默认空数组。
初始化函数
有三个初始化函数,一般初始化都会使用初始默认值_head和_tail最初都指向索引0(不代表_head=_tail就是空数组),_size=0长度为0。
入队
突然想起,军训期间被罚做完俯卧撑后,教官大喊入队。
入队的方法:Enqueue(T item)
入队看起来很简单,通过_tail拿到队尾索引,将元素插入队尾即可。但是这里有一点小细节:
下个元素的队尾索引 = (当前队尾索引 + 1) % 数据数组长度,这里的数据数组长度不等同与队列元素数量。此公式与抑制了队尾索引不会超过数据数组长度,从而避免了数据溢出的产生(同时也会导致队尾索引会小于队头索引,需要分情况进行处理)。
出队
出队有两种方法
(1)public T Peek():返回位于 Queue 开始处的对象但不将其移除。
(2)public T Dequeue():移除并返回位于 Queue 开始处的对象。
Peek()简单粗暴,通过索引直接返回数据。
Queue没有提供Remove方法,但是Dequeue具有删除功能并返回头元素。被移除的元素直接指向null(空引用)。并且头元素索引向前移动(这是一个跑圈圈的游戏,你懂的)。
查询
Contains(T item)判断队列中是否至少包含一个匹配的元素存在 是则返回true,否则返回false。
容量调整
容量调整,可以重置队列空间,如果元素数小于当前容量的 90%,将容量设置为 Queue中的实际元素数。
最后
(1)Queue可以通过TrimExcess()方法,将容量下降到实际元素的数量.
(2)Queue允许重复的元素。
(3)Queue和Stack主要是用来存储临时信息的。(下篇文章写Stack,一种后进先出的集合)
本系列持续更新,敬请关注
有投入,有产出。(注:非基础性,主要涉及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,一种后进先出的集合)
相关文章推荐
- 从源码安装Mysql/Percona 5.5
- 如何在 Linux/Windows/MacOS 上使用 .NET 进行开发
- 如何在 Linux 中安装微软的 .NET Core SDK
- C#.NET获取拨号连接的宽带连接方法
- C#.Net ArrayList的使用方法
- 浅析Ruby的源代码布局及其编程风格
- PowerShell中使用.NET将程序集加入全局程序集缓存
- .net(c#)中的new关键字详细介绍
- 由vbs sort引发.NET Framework之间的关系说明
- C#难点逐个击破(6):C#数据类型与.net framework数据类型
- .NET中的async和await关键字使用及Task异步调用实例
- 基于.NET平台常用的框架和开源程序整理
- .Net中的json操作类用法分析
- C#队列Queue用法实例分析
- .net实现序列化与反序列化实例解析
- .NET中的Timer类型用法详解
- 关于.net(C#)中的跨进程访问的问题
- .NET实现父窗体关闭而不影响子窗体的方法
- 基于.Net中的数字与日期格式化规则助记词的使用详解
- .NET微信公众号开发之公众号消息处理