在平常生活中,队列的例子比比皆是,例如在车展排队买票,排在队头的处理完离开,后来的必须在队尾排队等候。在程序设计中,队列也有着普遍的应用,例如计算机的任务调度系统、为了削减高峰时期订单请求的消息队列等等。与栈相似,队列也是属于操做受限的线性表,不过队列是只容许在一端进行插入,在另外一端进行删除。在其余数据结构如树的一些基本操做中(好比树的广度优先遍历)也须要借助队列来实现,所以这里咱们来看看队列。html
队列(queue)是只容许在一端进行插入操做,而在另外一端进行删除操做的线性表。它是一种先进先出(First In First Out)的线性表,简称FIFO。容许插入的一端称为队尾,容许删除的一端称为队头。node
(1)入队(Enqueue):将一个数据元素插入队尾;redis
(2)出队(Dequeue):读取队头节点数据并删除该节点;算法
既然队列也属于特殊的线性表,那么其实现也会有两种形式:顺序存储结构和链式存储结构。首先,对于Queue,咱们但愿可以提供如下几个方法供调用:数据库
Queue<T>() 数组 |
建立一个空的队列服务器 |
void Enqueue(T s) 数据结构 |
往队列中添加一个新的元素架构 |
T Dequeue() 并发 |
移除队列中最先添加的元素 |
bool IsEmpty() |
队列是否为空 |
int Size() |
队列中元素的个数 |
与Stack不一样,在队列中咱们须要定义一个head队头“指针”和tail队尾“指针”,当新元素入队时tail+1,当老元素出队时head+1。下面重点来看看Enqueue和Dequeue两个方法的代码实现。
(1)入队:Enqueue
public void EnQueue(T item) { if (Size == items.Length) { // 扩大数组容量 ResizeCapacity(items.Length * 2); } items[tail] = item; tail++; size++; }
新元素入队后,tail队尾指针向前移动指向下一个新元素要插入的位置;这里仍然模仿.NET中的实现,在数组容量不足时及时进行扩容以容纳新元素入队。
(2)出队:Dequeue
public T DeQueue() { if (Size == 0) { return default(T); } T item = items[head]; items[head] = default(T); head++; if (head > 0 && Size == items.Length / 4) { // 缩小数组容量 ResizeCapacity(items.Length / 2); } size--; return item; }
在对老元素进行出队操做时,首先取得head指针所指向的老元素,而后将head指针向前移动一位指向下一个将出队的老元素。这里将要出队的元素所在数组中的位置重置为默认值。最后判断容量是否太小,若是是则进行数组容量的缩小。
下面是完整的队列模拟实现代码,仅供参考,这里就再也不作基本功能测试了,有兴趣的读者能够自行测试:
/// <summary> /// 基于数组的队列实现 /// </summary> /// <typeparam name="T">类型</typeparam> public class MyArrayQueue<T> { private T[] items; private int size; private int head; private int tail; public MyArrayQueue(int capacity) { this.items = new T[capacity]; this.size = 0; this.head = this.tail = 0; } /// <summary> /// 入队 /// </summary> /// <param name="item">入队元素</param> public void EnQueue(T item) { if (Size == items.Length) { // 扩大数组容量 ResizeCapacity(items.Length * 2); } items[tail] = item; tail++; size++; } /// <summary>v /// 出队 /// </summary> /// <returns>出队元素</returns> public T DeQueue() { if (Size == 0) { return default(T); } T item = items[head]; items[head] = default(T); head++; if (head > 0 && Size == items.Length / 4) { // 缩小数组容量 ResizeCapacity(items.Length / 2); } size--; return item; } /// <summary> /// 重置数组大小 /// </summary> /// <param name="newCapacity">新的容量</param> private void ResizeCapacity(int newCapacity) { T[] newItems = new T[newCapacity]; int index = 0; if (newCapacity > items.Length) { for (int i = 0; i < items.Length; i++) { newItems[index++] = items[i]; } } else { for (int i = 0; i < items.Length; i++) { if (!items[i].Equals(default(T))) { newItems[index++] = items[i]; } } head = tail = 0; } items = newItems; } /// <summary> /// 栈是否为空 /// </summary> /// <returns>true/false</returns> public bool IsEmpty() { return this.size == 0; } /// <summary> /// 栈中节点个数 /// </summary> public int Size { get { return this.size; } } }
跟Stack链式存储结构不一样,在Queue链式存储结构中须要设置两个节点:一个head队头节点,一个tail队尾节点。如今咱们来看看在链式存储结构中,如何实现Enqueue与Dequeue两个方法。
(1)入队:Enqueue
public void EnQueue(T item) { Node<T> oldLastNode = tail; tail = new Node<T>(); tail.Item = item; if(IsEmpty()) { head = tail; } else { oldLastNode.Next = tail; } size++; }
入队操做就是在链表的末尾插入一个新节点,将原来的尾节点的Next指针指向新节点。
(2)出队:Dequeue
public T DeQueue() { T result = head.Item; head = head.Next; size--; if(IsEmpty()) { tail = null; } return result; }
出队操做本质就是返回链表中的第一个元素即头结点,这里能够考虑到若是队列为空,将tail和head设为null以加快垃圾回收。
模拟的队列链式存储结构的完整代码以下,这里就再也不作基本功能测试了,有兴趣的读者能够自行测试:
/// <summary> /// 基于链表的队列节点 /// </summary> /// <typeparam name="T"></typeparam> public class Node<T> { public T Item { get; set; } public Node<T> Next { get; set; } public Node(T item) { this.Item = item; } public Node() { } } /// <summary> /// 基于链表的队列实现 /// </summary> /// <typeparam name="T">类型</typeparam> public class MyLinkQueue<T> { private Node<T> head; private Node<T> tail; private int size; public MyLinkQueue() { this.head = null; this.tail = null; this.size = 0; } /// <summary> /// 入队操做 /// </summary> /// <param name="node">节点元素</param> public void EnQueue(T item) { Node<T> oldLastNode = tail; tail = new Node<T>(); tail.Item = item; if(IsEmpty()) { head = tail; } else { oldLastNode.Next = tail; } size++; } /// <summary> /// 出队操做 /// </summary> /// <returns>出队元素</returns> public T DeQueue() { T result = head.Item; head = head.Next; size--; if(IsEmpty()) { tail = null; } return result; } /// <summary> /// 是否为空队列 /// </summary> /// <returns>true/false</returns> public bool IsEmpty() { return this.size == 0; } /// <summary> /// 队列中节点个数 /// </summary> public int Size { get { return this.size; } } }
首先,咱们来看看下面的情景,在数组容量固定的状况下,队头指针以前有空闲的位置,而队尾指针却已经指向了末尾,这时再插入一个元素时,队尾指针会指向哪里?
图1
从图中能够看出,目前若是接着入队的话,因数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,咱们的队列在下标为0和1的地方仍是空闲的。咱们把这种现象叫作“假溢出”。现实当中,你上了公交车,发现前排有两个空座位,然后排全部座位都已经坐满,你会怎么作?立马下车,并对本身说,后面没座了,我等下一辆?没有这么笨的人,前面有座位,固然也是能够坐的,除非坐满了,才会考虑下一辆。
因此解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。咱们把队列的这种头尾相接的顺序存储结构称为循环队列。在循环队列中须要注意的几个问题是:
(1)入队与出队的索引位置如何肯定?
这里咱们能够借助%运算对head和tail两个指针进行位置肯定,实现方式以下所示:
// 移动队尾指针 tail = (tail + 1) % items.Length; // 移动队头指针 head = (head + 1) % items.Length;
(2)在队列容量固定时如何判断队列空仍是队列满?
①设置一个标志变量flag,当head==tail,且flag=0时为队列空,当head==tail,且flag=1时为队列满。
②当队列空时,条件就是head=tail,当队列满时,咱们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。以下图所示:
图2
从上图能够看出,因为tail可能比head大,也可能比head小,因此尽管它们只相差一个位置时就是满的状况,但也多是相差整整一圈。因此若队列的最大尺寸为QueueSize,那么队列满的条件是 (tail+1)%QueueSize==head(取模“%”的目的就是为了整合tail与head大小为一个问题)。好比上面这个例子,QueueSize=5,图中的左边front=0,而rear=4,(4+1)%5=0,因此此时队列满。再好比图中的右边,front=2而rear=1。(1+1)%5=2,因此此时队列也是满的。
(3)因为tail可能比head大,也可能比head小,那么队列的长度如何计算?
当tail>head时,此时队列的长度为tail-head。但当tail<head时,队列长度分为两段,一段是QueueSize-head,另外一段是0+tail,加在一块儿,队列长度为tail-head+QueueSize。所以通用的计算队列长度公式为:(tail-head+QueueSize)%QueueSize。
队列在实际开发中应用得很是普遍,这里来看看在互联网系统中常见的一个应用场景:消息队列。“消息”是在两台计算机间传送的数据单位。消息能够很是简单,例如只包含文本字符串;也能够更复杂,可能包含嵌入对象。消息被发送到队列中,“消息队列”是在消息的传输过程当中保存消息的容器。
在目前普遍的Web应用中,都会出现一种场景:在某一个时刻,网站会迎来一个用户请求的高峰期(好比:淘宝的双十一购物狂欢节,12306的春运抢票节等),通常的设计中,用户的请求都会被直接写入数据库或文件中,在高并发的情形下会对数据库服务器或文件服务器形成巨大的压力,同时呢,也使响应延迟加重。这也说明了,为何咱们当时那么地抱怨和吐槽这些网站的响应速度了。当时2011年的京东图书促销,曾一直出如今购物车中点击“购买”按钮后一直是“Service is too busy”,其实就是由于当时的并发访问量过大,超过了系统的最大负载能力。固然,后边,刘强东临时购买了很多服务器进行扩展以求加强处理并发请求的能力,还请了信息部的人员“喝茶”,如今京东已是超大型的网上商城了,我也有同窗在京东成都研究院工做了。
从京东当年的“Service is too busy”不难看出,高并发的用户请求是网站成长过程当中必不可少的过程,也是一个必需要解决的难题。在众多的实践当中,除了增长服务器数量配置服务器集群实现伸缩性架构设计以外,异步操做也被普遍采用。而异步操做中最核心的就是使用消息队列,经过消息队列,将短期高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务,改善网站系统的性能。在京东之类的电子商务网站促销活动中,合理地使用消息队列,能够有效地抵御促销活动刚开始就开始大量涌入的订单对系统形成的冲击。
虽然队列有顺序存储和链式存储两种存储方式,但在.NET中使用的是顺序存储,它所对应的集合类是System.Collections.Queue与System.Collections.Generic.Queue<T>,二者结构相同,不一样之处仅在于前者是非泛型版本,后者是泛型版本的队列。它们都属于循环队列,这里咱们经过Reflector来重点看看泛型版本的实现。
咱们来看看在.NET中的Queue<T>是如何实现入队和出队操做的。首先来看看入队Enqueue方法:
public void Enqueue(T item) { if (this._size == this._array.Length) { int capacity = (this._array.Length * 200) / 100; if (capacity < (this._array.Length + 4)) { capacity = this._array.Length + 4; } this.SetCapacity(capacity); } this._array[this._tail] = item; this._tail = (this._tail + 1) % this._array.Length; this._size++; this._version++; }
能够看出,与咱们以前所实现的Enqueue方法相似,首先判断了队列是否满了,若是满了则进行扩容,不一样之处在咱们是直接*2倍,这里是在原有容量基础上+4。因为是循环队列,对tail指针使用了%运算来肯定下一个入队位置。
咱们再来看看Dequeue方法时怎么实现的:
public T Dequeue() { if (this._size == 0) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyQueue); } T local = this._array[this._head]; this._array[this._head] = default(T); this._head = (this._head + 1) % this._array.Length; this._size--; this._version++; return local; }
一样,与以前相似,不一样之处在于判断队空时这里直接抛了异常,其次因为是循环队列,head指针也使用了%运算来肯定下一个出队元素的位置。
(1)程杰,《大话数据结构》
(2)陈广,《数据结构(C#语言描述)》
(3)段恩泽,《数据结构(C#语言版)》
(4)yangecnu,《浅谈算法与数据结构:—栈和队列》
(5)李智慧,《大型网站技术架构:核心原理与案例分析》
(6)Edison Chou,《Redis初探:消息队列》