本文整理来源 《轻松学算法——互联网算法面试宝典》/赵烨 编著前端
什么是队列?队列就是一个队伍。队列和栈同样,由一段连续的存储空间组成,是一个具备自身特殊规则的数据结构。栈是后进先出的规则,队列恰好相反,是一个先进先出(FIFO,First In First Out)或者说是后进后出(LILO,Last In Last Out)的数据结构。面试
队列是一种受限的数据结构,插入操做只能从一端操做,这一端叫作队尾;二移除操做也只能从另外一端操做,这一段叫做对头。算法
咱们将没有元素的队列称为空队。往队列中插入元素的操做叫做入队,相应的,从队列中移除元素的操做叫做出队。设计模式
通常而言,队列的实现有两种方式:数组和链表。用数组实现队列有两种方式,一种是顺序队列,一种是循环队列。数组
用数组实现队列,若出现队列满了的状况,则这时就算有新的元素须要入队,也没有位置。此时通常的选择是要么丢掉,要么等待,等待时间由程序控制。服务器
顺序队列会有两个标记,一个是对头位置(head),一个是下一个元素能够插入的队尾位置(tail)。一开始两个标记都指向数组下表为0的位置。数据结构
在插入元素以后,tail标记就会加1,如入队三个元素,分别是A、B、C,则当前标记即存储状况。head指向0,tail指向3。异步
当head为0时,tail为3.接下来进行出队操做,出队一个元素,head指向的位置则加1。好比进行一次出队操纵以后,顺序队列存储状况。head指向1,tail指向3。测试
所以,在顺序队列中,对队列中元素的个数咱们能够用tail减去head计算。当head与tail相等时,队列为空队,当tail达到数组的长度,也就是队列存储以外的位置时,说明这个队列已经没法容纳其余元素入队了。空间是否满了?并无,因为两个标记只增不减,因此两个标记最终都会到数组的最后一个元素以外,这是虽然数组是空的,但也没法再往队列里加入元素了。this
当队列中没法再加入元素时,咱们称之为“上溢”; 当顺序队列还有空间却没法入队时,咱们称之为“假上溢”;若是空间真的满了,则咱们称之为“真上溢”;若是队列是空的,则执行出队操做,此时队列里没有元素,能不能出队,咱们称之为“下溢”。
怎么解决顺序队列的“假上溢”问题,这时就须要采用循环队列了。
当顺序队里出现假上溢时,其实数组前端还有空间,咱们能够不把标记指向数组外的地方,只须要把这个标记从新指向开始处就可以解决。想一想一下这个数组首尾相接,成为一个圈。存储结构仍是在一个数组上。
通常而言。咱们在对head或者tail加1时,为了方便,可直接对结果取余数组长度,获得咱们徐亚的数组长度。另外因为顺序队列存在“假上溢”的问题,全部在实际使用过程当中都是使用循环队列来实现的。
可是循环队列中会出现这样一种状况:当队列没有元素时,head等于tail,而当队列满了时,head也等于tail。为了区分这两种状态,通常在循环队列中规定队列长度只能为数组总长度减1,即有一个位置不放元素。所以,当head等于tail时,说明队列为空队,而当head等于(tail+1)%length时,说明队满。
代码以下
public class ArrayQueue<T> { private final Object[] items; private int head = 0; private int tail = 0; /** * 初始化队列 * * @param capacity 队列长度 */ public ArrayQueue(int capacity) { this.items = new Object[capacity]; } /** * 入队 * * @param item 入队元素 * @return 是否入队 */ public boolean put(T item) { if (head == (tail + 1) % items.length) { //表示队满 return false; } items[tail] = item; //tail 标记日后移一位 tail = (tail + 1) % items.length; return true; } /** * 获取队列头元素,不出队 * * @return 队列头元素 */ @SuppressWarnings("unchecked") public T peek() { if (head == tail) { return null; } return (T)items[head]; } /** * 出队 * * @return 头部元素 */ @SuppressWarnings("unchecked") public T poll() { if (head == tail) { return null; } T item = (T)items[head]; //把没有用的元素赋空值,固然也能够不设置,标记移动了,以后会被覆盖。尽可能仍是设置null items[head] = null; //head标记日后移动一位 head = (head + 1) % items.length; return item; } public boolean isFull() { return head == (tail + 1) % items.length; } public boolean isEmpty() { return head == tail; } /** * 队列元素数 * * @return 队列元素数 */ public int size() { if (tail >= head) { return tail - head; }else { return tail + items.length - head; } } }
测试代码以下
public class ArrayQueueTest { @Test public void main(){ ArrayQueue<String> queue = new ArrayQueue<>(4); Assert.assertTrue("添加A失败",queue.put("A")); Assert.assertTrue("添加B失败",queue.put("B")); Assert.assertTrue("添加C失败",queue.put("C")); Assert.assertTrue("添加D成功",!queue.put("D")); //队列已满,而且D元素没有入队成功 Assert.assertTrue("队列未满",queue.isFull()); Assert.assertEquals("队列中元素数不为3",3,queue.size()); //获取头元素但不出队 Assert.assertEquals("头元素不为A","A",queue.peek()); Assert.assertEquals("出队A失败","A",queue.poll()); Assert.assertEquals("出队B失败","B",queue.poll()); Assert.assertEquals("出队C失败","C",queue.poll()); //队列为空队 Assert.assertTrue("队列不为空",queue.isEmpty()); } }
以上代码中,声明为4,可是使用的时候只能放入3个元素,采用的是初始化数组时多设置一个位置来解决问题的;也能够经过增长一个变量来记录元素的个数去解决问题,不须要两个标记去肯定是队空仍是队满,元素也能放满而不用空出一位了。
队列的特色就是先进先出。出队的一头是队头,入队的一头是队尾。固然,队列通常都会规定一个有限的长度,叫作队长。
队列在实际开发当中很经常使用。在通常程序中会将队列做为缓冲器或者解藕使用。
某品牌的手机推出新型号,想要购买就须要上网预定,等到了开抢时间就得赶忙打开网页守着,疯狂刷新页面。疯狂的点击抢购按钮。通常在每次秒杀活动中提供的手机只有几千部。假设有两百万的人抢购,那么从开抢的这一秒,两百万人都开始向服务器发送请求。若是服务器都能直接处理请求,把抢购结果马上告诉用户,同时为请购成功的用户生成订单,让用户付款购买手机,则这对服务器的要求很高,很难实现。能够采用排队的方式解决。把这些请求按顺序放入队列的队尾中,而后提示用户“正在排队中......”,接下来用户开始排队:并且这个对列的另外一端,也就是队头会有一些服务器去处理,根据前后顺序告知用户抢购结果。
这就出现了抢购手机时,抢购界面稍后才会告诉咱们抢购结果的状况。
这种方式也叫作异步处理。异步与同步是相对的。同步是在一个调用执行完成以后,等待调用结束返回;而异步不会马上返回结果,返回结果的时间是不可预料的,在另外一端的服务器处理完以后才会有结果,如何通知执行的结果又是另外一回事。
这个模式就像有一个传送带,生产者在传送带这头将生产的货物放上去,消费者在另外一头逐个地将货物从传送带上取下来。这种设计设计模式的原理也比较简单,即存在一个队列,若干个生产者同时向队列中添加元素,而后若干个消费者从队列中获取元素。
对于生产者和消费者的设计模式来讲,有一点很是重要,那就是生产速度要和消费的速度持平。若是生产太快,而消费得太慢,那么队列就会很长。而对于计算机来讲,队列太长所占用的空间也会较大。