队列
队列是一种先进先出的数据结构,基本操作就是入队,放一个数据到队列尾部;出队,从队列头部取一个元素. 队列与栈一样,也是一种操作受限的线性表数据结构;
栈与队列
顺序队列与链式队列
队列可以使用数组实现,也可以使用链表实现,用数组实现的队列叫顺序队列,用链表实现的队列叫链式队列
数组的实现方式:
// 用数组实现的队列
public class ArrayQueue {
// 数组:items,数组大小:n
private String[] items;
private int n = 0;
// head 表示队头下标,tail 表示队尾下标
private int head = 0;
private int tail = 0;
// 申请一个大小为 capacity 的数组
public ArrayQueue(int capacity) {
items = new String[capacity];
n = capacity;
}
// 入队
public boolean enqueue(String item) {
// 如果 tail == n 表示队列已经满了
if (tail == n) return false;
items[tail] = item;
++tail;
return true;
}
// 出队
public String dequeue() {
// 如果 head == tail 表示队列为空
if (head == tail) return null;
// 为了让其他语言的同学看的更加明确,把 -- 操作放到单独一行来写了
String ret = items[head];
++head;
return ret;
}
}
对于栈来说,只需一个栈顶指针即可,但队列需要两个指针,一个是head指针,指向队头;一个是tail指针,指向队尾;
如下图所示,当a,b,c,d依次入队之后,队列中head指针指向下标为0的位置,tail指针指向下标为4的位置.
当两次出队操作之后,队列中head指针指向下标为2的位置,tail指针仍然指向下标为4的位置
随着不停的进行入队,出队操作,head和tail都会持续后移动,当tail移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了;如何解决?
用数据搬移解决,每次进行出队操作都相当于删除数组下标为0的数据,要搬移整个队列中数据,这样出队操作的时间复杂度就会从原来的O(1)变为O(n),能不能优化一下
实际上,出队时可以不用搬移数据,如果没有空闲时间,只需在入队时再集中触发一次数据的搬移操作改造一下equeue()的实现
// 入队操作,将 item 放入队尾
public boolean enqueue(String item) {
// tail == n 表示队列末尾没有空间了
if (tail == n) {
// tail ==n && head==0,表示整个队列都占满了
if (head == 0) return false;
// 数据搬移
for (int i = head; i < tail; ++i) {
items[i-head] = items[i];
}
// 搬移完之后重新更新 head 和 tail
tail -= head;
head = 0;
}
items[tail] = item;
++tail;
return true;
}
当队列指针移动到数组的最右边后,如果有新的数据入队,就可以将head到tail之间的数据,整体搬移到数组0到tail-head的位置
基于链表的队列实现方法
需要两个指针: head指针和tail指针,分别指向链表的第一个节点和最后一个节点,入队时,tail->next=new_node,tail=tail->next;出队时,head=head->next
循环队列
在用数组实现队列时,在tail==n时,会有数据搬移操作,这样会影响入队操作性能;循环队列,长得像一个环,原本数组是有头有尾的,现在把收尾相连,扳成一个环;
如图所示,队列大小为8,当前head=4,tail=7,当有一个新元素a入队时,放入下标为7的位置,但此时不将tail更新为8,而是将其在环中后移一位,到下标为0的位置,当再有一个元素b入队时,然后tail加1更新为1;如图所示
循环队列避免了数据的搬移操作,但是实现一个循环队列,关键是要确定好对列空和队满的判定条件
队列为空的判断条件仍然是head==tail;队满的判断条件要总结规律,队满时 (tail+1)%n=head
public class CircularQueue {
// 数组:items,数组大小:n
private String[] items;
private int n = 0;
// head 表示队头下标,tail 表示队尾下标
private int head = 0;
private int tail = 0;
// 申请一个大小为 capacity 的数组
public CircularQueue(int capacity) {
items = new String[capacity];
n = capacity;
}
// 入队
public boolean enqueue(String item) {
// 队列满了
if ((tail + 1) % n == head) return false;
items[tail] = item;
tail = (tail + 1) % n;
return true;
}
// 出队
public String dequeue() {
// 如果 head == tail 表示队列为空
if (head == tail) return null;
String ret = items[head];
head = (head + 1) % n;
return ret;
}
}
阻塞队列
就是在队列基础上增加了阻塞操作,就是在队列为空的时候,从队头取数据会被阻塞,因此此时还没有数据可取,直到队列中有数据才能返回,如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回
线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略如何实现?
一般两种处理策略,第一种是非阻塞的处理方式,直接拒绝任务请求;另一种是阻塞的处理方式,请请求排队,等到有空闲线程时,取出排队的请求继续处理; 如何存储排队的请求?
若希望公平的处理每个排队的请求,先进者先服务,所以队列这种数据结构很适合来存储排队请求;
基于链表的实现方式,可以实现一个支持无限排队的无界队列,但是可能导致过多的请求排队等待,请求处理的响应时间过长,故针对响应时间比较敏感的系统,基于链表实现无限排队的线程池是不合适的;
基于数组实现的有界队列,队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说更加合理