队列
队列是一种常用的线性数据结构,具有先进先出的特点。队列中元素的插入和删除操作分别在队列的两端进行,新元素被插入到队列的尾部,而原有元素则从队列的头部被删除。
队列中包含两个基本操作 :入队(enqueue)和出队(dequeue)。其中,入队操作会将一个元素添加到队列的尾部,而出队操作会从队列的头部删除一个元素,并返回该元素的值。此外,队列还支持其他常见操作,例如 获取队列长度、判断队列是否为空、清空队列等。
java中提供了多种不同类型的队列实现类,例如:LinkedList、ArrayDeque、PriorityQueue等。以下是一个使用Java中内置的LinkedList实现队列的代码:
public class QueueExample {
public static void main(String[] args) {
//创建一个队列对象
Queue<String> queue = new LinkedList<>();
//入队操作
queue.offer("a");
queue.offer("b");
queue.offer("c");
//出队操作
String firstElement = queue.poll();
System.out.println("出队元素:" + firstElement);
//获取队列长度
int queueSize = queue.size();
System.out.println("队列长度:" + queueSize);
//判断队列是否为空
boolean isEmpty = queue.isEmpty();
System.out.println("队列是否为空:" + isEmpty);
//清空队列
queue.clear();
}
}
该代码创建了一个队列对象queue,并向其中插入三个元素。接着,通过调用poll方法从队列中删除第一个元素,并输出其值。然后获取队列长度、判断队列是否为空并清空队列等操作。需要注意的是,在使用队列时需要遵循先进先出的原则,以确保数据处理的正确性和公正性。
基于数组实现的队列
//用数组实现的队列
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依次入队之后,队列中的head指针指向下标为0的位置,tail指针指向下标为4的位置。
当调用两次出队操作之后,队列中head指针指向下标为2的位置,tail指针仍然指向下标为4的位置
随着不停的入队(tail后移)、出队(head后移)操作,head和tail都会持续往后移动。当tail移动到最右边,此时,数组就算还有空闲空间,也无法继续往队列中添加数据了。这个问题如何解决?
//入队操作,将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;
}
在出队时可以不用搬移数据。如果没有空闲空间,我们只需要在入队时,再集中触发一次数据的搬移操作。
当队列的tail指针移动到数组的最右边后,如果有新的数据入队,我们可以将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入队时,我们将b放入下标为0的位置,然后tail加1更新为1。因此,在a,b依次入队后,循环队列中的元素会变成如下图的所示:
想要写出循环队列的代码最关键的是,确定好队空和队满的判定条件。
在用数组实现的非循环队列中,队满的判断条件是tail == n,队空的判断条件是head == tail。对于如何判断队空和队满,可以这样来想,队列为空的判断条件仍然是head == tail。但队列满的判断条件就稍微复杂一些。通过下图的队满来看
不难发现(3+1)%8=4,由于tail = 3,head = 4,n = 8可以得出公式(tail+1)%n=head。
因此,当队满时,tail指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。如下代码:
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 equeue(String item){
//队列满了
if((tail + 1)%n = head){
return false;
}
items(tail) = item;
tail = (tail + 1)%n;
return ture;
}
//出队
public String dequeue(){
//如果head == tail表示队列为空
if(head == tail){
return null;
}
String ret = items[head];
head = (head + 1)%n;
return ret;
}
}
阻塞队列
阻塞队列是在队列基础上增加阻塞操作。其实就是队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
就是“生产者-消费者模型”,可以有效地协调生产和消费的速度。当“生产者”生产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满。这个时候,生产者就阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。
不仅如此,还可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。比如上面的例子,可以多配置几个“消费者”,来应对一个“生产者”。
并发队列
在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列?因此把实现线程安全的队列称为 :并发队列。最简单直接的实现方式是直接在enqueue()、dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用CAS原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。