C# Queue源码剖析

源代码版本为 .NET Framework 4.6.1

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

有投入,有产出。

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

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

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

概念

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

C# Queue源码剖析_第1张图片

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

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

循环队列

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

循环队列中,由于入队时尾指针向前追赶头指针;出队时头指针向前追赶尾指针,造成空队列和满队列时头尾指针均相等。因此,无法通过条件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,线性表)