队列
队列,就是排队,先到的站前面,先离开,后到的排后面,后离开。对应到计算机中,就是添加元素在队尾,删除元素是在队头,先进先出或后进后出。添加元素也叫入队(enqueue),删除元素也叫出队(dequeue)。当然还可以查看队头元素,队中元素个数,以及是否为空,所以队列提供了API 就是enqueue, dequeue,getFront, size, isEmpty。
使用单链表实现队列
队列在尾部添加元素,在头部删除元素。那就让链表头作为队列的头部,因为链表头部容易执行删除操作(出队)。链表尾部只能作为队列的尾部,执行插入操作(入队)。但链表尾部执行插入操作,有一个问题,那就是每次都要遍历整个链表,找到最后一个元素,才能执行插入操作。为了减少遍历,要再维护一个尾指针,指向链表的尾部。因此,使用单链表实现一个队列,链表需要维护两个指针,头指针和尾指针。头指针指向链表的头部,用于出队。尾指针指向链表尾部,用于入队。
public class LinkedQueue<T> {
private class Node {
T data;
Node next;
Node(T data) {
this.data = data;
}
}
private Node firstNode; //头指针,队头
private Node lastNode; // 尾指针,队尾
private int size;
public void enqueue(T data){}
public T dequeue(){}
public int size(){}
public boolean isEmpty(){}
public T getFront(){}
public void clear(){}
}
enqueue的实现,就是向链表尾部插入一个节点。创建一个新节点,如果队列(链表)为空,直接让头尾指针都指向它
如果链表不空,让尾节点的next指向它,同时更新尾指针的指向,让它指向最新的尾节点
public void enqueue(T data){ Node newNode = new Node(data); if(isEmpty()){ firstNode = lastNode =newNode; } else { lastNode.next = newNode; lastNode = newNode; }
size++; }
dequeue的实现,就是链表头部删除一个节点。链表为空,肯定是不能被删除的,如果链表不空,取出第一个节点,然后让头指针指向它的next节点就好了,
要注意的是一直删除,头指针会指向null,也就是链表中没有元素了,尾指针也要指向null
public T dequeue(){ if(isEmpty()){ throw new RuntimeException("链表为空"); } T frontData = firstNode.data; firstNode = firstNode.next; if(firstNode == null){ lastNode = null; } size--; return frontData; }
其它几个实现比较简单
public int size(){ return size; } public boolean isEmpty(){ return firstNode == null; } public T getFront(){ if(isEmpty()){ throw new RuntimeException("链表为空"); } return firstNode.data; } public void clear(){ firstNode = null; lastNode = null; }
数组实现(循环数组)
数组实现队列,queue[0]成为队列的前端,frontIndex和backIndex分别是队列前端和后端元素的索引。
但删除元素时会发生什么?如果坚持新的前端条目在queue[0]中,需要将每个数组条目向数组的开头移动一个位置。这种安排会使出队操作效率低下。相反,当我们删除队列的最前面的条目时,我们可以将其他数组条目保留在其当前位置。出队两次,删除队列前面两个元素
当不停地入队或出队后,可能出现以下情况
队列中只有3个元素,但头部索引和尾部索引却到了数组的尾部,如果再添加元素,是扩充数组,还是添加到数组的前面?添加前面能充分得用空间
数组表现得像一个圆形,第一个元素跟着最后一个元素后面,计算数组索引的时候,使用取模运算。当backIndex = 49时,再添加一个元素,backIndex成了0,正好是(backIndex+1)%数组的长度。当删除元素的时,也是一样,frontIndex = (frontIndex + 1)%数组的长度。但这也带来一个问题,怎么判断队列是满的,队列是空的?当不停地向队列中添加元素的时候,队列满了
当删除几个元素时,比如5个,frontIndex变成2(47,48,49,0,1)
当继续删除,剩下一个元素的时候,
删除最后一个元素
可以发现,frontIndex = backIndex + 1; 和队列是满的的判断条件一致。因此,仅通过backIndex和frontIndex 无法判断队列是满的,还是空的。因此,在使用循环数组时,可以规定一个数组空间不用,假设7个元素的数组,只能使用6个位置,数组为空,frontIndex 为0, backIndex为数组最后一个索引的位置,就是6
当添加一个元素的时候,backIndex变成了0,
继续添加元素,直到满,
再删除一个元素,再添加一个元素
队列还是满的。再删除一个,再添加一个,
队列还是满的,一直删除,剩下最后一个,和空队列
可以发现,空的,没有使用的空间的索引,永远比backIndex 大1,比frontIndex 小1, 因此,frontIndex == (backIndex + 2) % 数组的长度,队列满了。面队列空的时候,frontIndex 比backIndex 大1, 所以frontIndex == (backIndex +1)% 数组的长度, 队列为空。
public class ArrayQueue<T> implements QueueInterface<T> { private T[] queue; private int frontIndex; private int backIndex; private static final int DEFAULT_CAPACITY = 50; public ArrayQueue() { this(DEFAULT_CAPACITY); } public ArrayQueue(int intialCapacity) { @SuppressWarnings("unchecked") T[] temp = (T[]) new Object[intialCapacity + 1]; queue = temp; frontIndex = 0; backIndex = intialCapacity; } @Override public void enqueue(T newEntry) { ensureCapacity(); backIndex = (backIndex + 1) % queue.length; queue[backIndex] = newEntry; } private void ensureCapacity() { } @Override public T dequeue() { if (isEmpty()) throw new RuntimeException("empty"); else { var front = queue[frontIndex]; queue[frontIndex] = null; frontIndex = (frontIndex + 1) % queue.length; return front; } } @Override public T getFront() { if (isEmpty()) throw new RuntimeException("empty"); else return queue[frontIndex]; } @Override public boolean isEmpty() { return frontIndex == ((backIndex + 1) % queue.length); } @Override public void clear() { for (int index = 0; index < queue.length - 1; index++) { queue[index] = null; } } }
看一下ensureCapacity。假设有7个元素的数组,已经满了,现在要扩展成14个元素,
ensureCapacity的实现方式
private void ensureCapacity() { if(frontIndex == (backIndex + 2) % queue.length) { var oldQueue = queue; int oldSize = queue.length; int newSize = oldSize * 2; @SuppressWarnings("unchecked") T[] tempQueue = (T[]) new Object[newSize]; queue = tempQueue; for (int i = 0; i < oldSize - 1; i++) { queue[i] = oldQueue[frontIndex]; frontIndex = (frontIndex + 1) % oldSize; } frontIndex = 0; backIndex = oldSize - 2; } }
使用队列模拟现实的队列,比如买奶茶,以测算奶茶店的服务能力。如果要统计1小时内的服务能力,可以计算,在一小时内的到达人数,服务人数,等待时间等等。怎么统计呢?1小时,可以分60分钟,每一分钟检测一次,有没有顾客来,如果有就加到队列中,如果没有,就看有没有顾客在服务,如要有,就继续服务,如果没有,服务下一位顾客。怎么知道有没有人来?由于每一个顾客的到达时间是随机的,可以使用一个随机数,如果生成的随机数小于一个阈值,就说明有顾客到,反之,则没有顾客到。 由于每个顾客的服务时间也不一样,可以再使用一个随机数,计算出服务时间。可以看出有两个类,WaitLine和Customer,在WaitLine中有到达人数(numberOfArrived),服务人数(numberServed), 等待时间(totalTimeWaited),在Customer中有到达时间(arriveTime),服务时间(transactionTime)和排队号码(customerNum)。
public class WaitLine { private int numberOfArrivals; private int numberServed; private int totalTimeWaited; private class Customer { int arriveTime; int transactionTime; int customerNum; Customer(int arriveTime, int transactionTime, int customerNum) { this.arriveTime = arriveTime; this.transactionTime = transactionTime; this.customerNum = customerNum; } } }
现在模拟一下队列的情形,顾客到来的时间是随机的,假设有50%的概率会来,那就表示,只要生成的随机数小于50%,就表明顾客到了,加入队列。顾客的服务时间也是不固定的,可以声明一个最大服务时间,然后和随机数相乘,假设最大服务时间是5s。顾客有没有在服务,就是看它的服务时间有没有到0,如果到了,就表示服务完成,到下一位顾客。
// duration: 要统计的服务时间区间,比如60分钟 // arrivalProbability:每秒钟顾管到达的概率, 比如50% // maxTransactionTime:每位顾客的最长服务时间 public void simulate(int duration, double arrivalProbability, int maxTransactionTime) { var line = new LinkedQueue<Customer>(); // 创建一个队列, var transactionTimeLeft = 0; // 每个顾客服务时间的剩余时间,表示一个顾客在不在服务
// clock就是每一秒,用户的到达时间和用户的服务时间都用clock记录 for (int clock = 0; clock < duration; clock++) { // 监测用户到没到,随机数小于规定的概率,表示有顾客到 if (Math.random() < arrivalProbability) { numberOfArrivals++; var transactionTime = (int) (Math.random() * maxTransactionTime + 1);//生成每位顾客的服务时间 var customer = new Customer(clock, transactionTime, numberOfArrivals); // 创建到达的顾客 line.enqueue(customer); } // 某位顾客是否还在服务中 if (transactionTimeLeft > 0) { transactionTimeLeft--; //还在服务中,继续服务,不过服务时间要减1 } else { if(!line.isEmpty()) { var nextCustomer = line.dequeue(); // 顾客离开队列,被服务 transactionTimeLeft = nextCustomer.transactionTime - 1; // 赋值服务时间,下次验证是不是在服务他 var waitingTime = clock - nextCustomer.arriveTime; // 每个用户等待服务的时间 totalTimeWaited = totalTimeWaited + waitingTime; // 整个队列中用户等待的时间 numberServed++; } } } }
测试一下
public static void main(String[] args) { WaitLine customerLine = new WaitLine(); customerLine.simulate(20, 0.5, 5); System.out.println(customerLine.numberServed); }