文章目录
一、什么是队列
队列是一种线性数据结构,它的特点是先进先出。在队列中,元素的添加(入队)操作在队尾进行,而元素的移除(出队)操作则在队头进行。因此,队列可以被简单地描述为一个“先进先出”的容器。在Java中,队列接口继承自Collection接口,并提供了丰富的方法来操作队列中的元素。
二、队列中常用的方法
1.add()和offer():添加元素
相同:未超出容量,从队尾压入元素,返回压入的那个元素。
区别:在超出容量时,add()方法会对抛出异常,offer()返回false
Queue<String> queue = new LinkedList<>();
queue.add("A");
queue.offer("B");
// 队列中的元素:[A, B]
2.remove()、poll():弹出(删除)元素
相同:容量大于0的时候,删除并返回队头被删除的那个元素。
区别:在容量为0的时候,remove()会抛出异常,poll()返回false
Queue<String> queue = new LinkedList<>();
queue.add("A");
queue.add("B");
String headElement = queue.remove();
// headElement 的值为 "A",队列中的元素:[B]
3.element()、peek():获取队头元素(不删除)
相同:容量大于0的时候,都返回队头元素。但是不删除。
区别:容量为0的时候,element()会抛出异常,peek()返回null。
Queue<String> queue = new LinkedList<>();
queue.add("A");
queue.add("B");
String headElement = queue.element();
// headElement 的值为 "A",队列中的元素:[A, B]
4.isEmpty():检查队列是否为空
Queue<String> queue = new LinkedList<>();
boolean isEmpty = queue.isEmpty();
// isEmpty 的值为 true
5.size():返回队列中元素的个数
Queue<String> queue = new LinkedList<>();
queue.add("A");
queue.add("B");
int size = queue.size();
// size 的值为 2
6.clear():清空队列中的所有元素
Queue<String> queue = new LinkedList<>();
queue.add("A");
queue.add("B");
queue.clear();
// 队列中的元素为空
三、在Java中常见队列
ArrayList:ArrayList可以被用作队列,通过在列表末尾添加元素,并使用remove(0)方法从列表的开头删除元素。但是,由于在列表的开头删除元素会导致后续元素的移动,因此对于大量的插入和删除操作来说,ArrayList的性能可能不是最佳选择。
LinkedList:LinkedList也可以用作队列。是Java中常用的双向链表实现,LinkedList实现了Queue接口,可以使用offer()方法在队列的末尾添加元素,使用poll()方法从队列的开头删除并返回元素。LinkedList对于插入和删除操作具有较好的性能,因为它使用了指针来链接元素,而不需要移动其他元素。
Queue<String> queue = new LinkedList<>();
queue.offer("a"); // 入队
queue.offer("b");
String element = queue.poll(); // 出队
System.out.println(element); // 输出:a
ArrayBlockingQueue:ArrayBlockingQueue是一个有界阻塞队列,底层使用数组实现。它有一个固定的容量,并且在插入或删除元素时可能会阻塞线程,直到满足特定的条件。
ArrayDeque:是一种基于数组的双端队列实现,它同样实现了Queue接口,并且在尾部添加和移除元素的操作具有较低的时间复杂度。以下是ArrayDeque的简单示例:
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(1); // 入队
queue.offer(2);
int element = queue.poll(); // 出队
System.out.println(element); // 输出:1
LinkedBlockingQueue:LinkedBlockingQueue是一个可选有界或无界的阻塞队列,底层使用链表实现。它具有类似于ArrayBlockingQueue的功能,但在内部实现上略有不同。
PriorityBlockingQueue:PriorityBlockingQueue是一个支持优先级的无界阻塞队列。它可以确保每次出队的元素都是队列中优先级最高的元素。元素按照它们的优先级顺序被插入和删除。
ConcurrentLinkedQueue:ConcurrentLinkedQueue是一个非阻塞无界队列,它适用于多线程环境。它使用链表实现,并且提供了高效的并发操作。
以上是Java中的一些常见队列实现。具体选择哪种队列取决于你的需求和使用场景。
四、Deque 接口分析
Deque接口是Java集合框架中定义的一个接口,它代表了一个双端队列(Double Ended Queue)。Deque是"双端队列"的缩写。Deque接口继承自Queue接口,并在其基础上提供了在队列两端进行添加、删除和检索元素的操作。Deque可以在队列的头部和尾部同时进行元素的插入和删除,因此可以作为队列、栈或双向队列使用。
Deque接口定义了以下主要方法和特性:
(1)添加元素:
void addFirst(E element): 将指定元素添加到双端队列的头部。
void addLast(E element): 将指定元素添加到双端队列的尾部。
boolean offerFirst(E element): 将指定元素添加到双端队列的头部,如果成功则返回true,如果队列已满则返回false。
boolean offerLast(E element): 将指定元素添加到双端队列的尾部,如果成功则返回true,如果队列已满则返回false。
(2)移除元素:
E removeFirst(): 移除并返回双端队列的头部元素,如果队列为空则抛出异常。
E removeLast(): 移除并返回双端队列的尾部元素,如果队列为空则抛出异常。
E pollFirst(): 移除并返回双端队列的头部元素,如果队列为空则返回null。
E pollLast(): 移除并返回双端队列的尾部元素,如果队列为空则返回null。
(3)获取头部和尾部元素:
E getFirst(): 获取双端队列的头部元素,但不移除它,如果队列为空则抛出异常。
E getLast(): 获取双端队列的尾部元素,但不移除它,如果队列为空则抛出异常。
E peekFirst(): 获取双端队列的头部元素,但不移除它,如果队列为空则返回null。
E peekLast(): 获取双端队列的尾部元素,但不移除它,如果队列为空则返回null。
Deque接口还提供了一些其他方法,如size()用于返回双端队列中的元素个数,isEmpty()用于判断双端队列是否为空,clear()用于清空双端队列中的所有元素等。
Deque接口的常见实现类包括ArrayDeque和LinkedList。ArrayDeque是一个基于数组实现的双端队列,支持高效的随机访问和动态扩展。LinkedList是一个基于链表实现的双端队列,支持高效的插入和删除操作。
通过使用Deque接口,我们可以方便地进行双端队列操作,如在队列的头部和尾部插入和删除元素,获取头部和尾部元素,以及判断队列是否为空。Deque在许多场景下都很有用,比如实现LRU缓存、实现任务调度等。
另外,需要注意的是,Deque接口还可以用作栈(LIFO)的数据结构。通过在队列头部执行插入和删除操作,可以实现栈的功能。常见的栈操作可以使用Deque接口中的以下方法来实现:
- void push(E element): 将元素推入栈顶,等同于addFirst(E element)。
- E pop(): 弹出并返回栈顶元素,等同于removeFirst()。
- E peek(): 获取栈顶元素,等同于peekFirst()。
所以,Deque接口是一个非常有用的接口,提供了双端队列的功能,既可以在队列的头部进行操作,也可以在尾部进行操作。它是Queue接口的扩展,可以方便地实现队列、栈和双向队列的功能,并提供了丰富的方法来操作和访问队列中的元素。
Java Deque 接口使用示例
当使用Java中的Deque接口时,我们同样需要选择具体的实现类来创建一个双端队列对象。下面是一个使用Deque接口的示例,以ArrayDeque实现类为例:
public class DequeExample {
public static void main(String[] args) {
// 创建一个Deque对象
Deque<String> deque = new ArrayDeque<>();
// 添加元素到双端队列
deque.addFirst("Apple");
deque.addLast("Banana");
deque.addLast("Orange");
// 获取双端队列头部和尾部元素
String first = deque.getFirst();
String last = deque.getLast();
System.out.println("头部元素:" + first);
System.out.println("尾部元素:" + last);
// 遍历双端队列并输出元素
System.out.println("双端队列元素(从头到尾):");
for (String element : deque) {
System.out.println(element);
}
// 移除双端队列头部和尾部元素
String removedFirst = deque.removeFirst();
String removedLast = deque.removeLast();
System.out.println("移除的头部元素:" + removedFirst);
System.out.println("移除的尾部元素:" + removedLast);
// 双端队列大小
int size = deque.size();
System.out.println("双端队列大小:" + size);
// 判断双端队列是否为空
boolean isEmpty = deque.isEmpty();
System.out.println("双端队列是否为空:" + isEmpty);
}
}
在这个示例中,我们使用ArrayDeque作为实现类创建了一个Deque对象。我们向双端队列中添加了三个元素:“Apple”、“Banana"和"Orange”。然后,我们使用getFirst()和getLast()方法分别获取双端队列的头部和尾部元素,并使用for-each循环遍历双端队列并输出每个元素。
PriorityQueue 的实现原理详解
PriorityQueue的实现原理是基于二叉堆(Binary Heap),它是一种特殊的完全二叉树结构,具有以下性质:
-
最小堆性质:在最小堆中,每个节点的值都小于或等于其子节点的值。也就是说,堆的根节点是最小的元素。
PriorityQueue使用一个数组来存储元素,并通过二叉堆的形式来组织这些元素。数组中的元素按照特定的顺序排列,满足最小堆的性质。在数组中,根节点位于索引0处,而对于任意位置i的节点,它的左子节点位于索引2i+1处,右子节点位于索引2i+2处。
当元素被添加到PriorityQueue时,它会被放置在数组的末尾,并按照以下步骤进行调整,以维护最小堆的性质:
-
上滤(Up-Heap)操作:新插入的元素会与其父节点进行比较。如果新插入的元素的优先级比父节点的优先级低(或者更大),则它会与父节点进行交换,直到满足最小堆的性质。
当从PriorityQueue中删除元素时,队列头部的元素被移除,并将数组的最后一个元素移动到头部位置。然后,这个元素会与其子节点进行比较,以保持最小堆的性质。
-
下滤(Down-Heap)操作:被移动到头部位置的元素会与其子节点进行比较。如果它的优先级比其中一个或两个子节点的优先级高(或者更小),则它会与较小的子节点进行交换。这个过程会递归地向下进行,直到满足最小堆的性质。
通过上述的上滤和下滤操作,PriorityQueue可以保持最小堆的性质,使得具有最高优先级的元素总是位于队列的头部。PriorityQueue的插入和删除操作的时间复杂度都是O(logN),其中N是队列中的元素个数。这是因为这些操作涉及到堆的调整,需要按照树的高度来进行操作。同时,PriorityQueue还支持高效的查找具有最高优先级的元素,时间复杂度为O(1)。
需要注意的是,PriorityQueue允许元素具有相同的优先级,但它们的顺序不一定是确定的。在这种情况下,PriorityQueue的行为是不保证的,具有相同优先级的元素可能会以任意顺序被取出。
优先队列(PriorityQueue)的使用示例:
public static void main(String[] args) {
//默认采用的是最小堆实现的
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10, new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return a - b; //if a>b 则交换,so这是递增序列
}
});
queue.offer(13);
queue.offer(9);
int len = queue.size();
for (int i = 0; i < len; i++) {
System.out.println(queue.poll());
}
//输出 9 13
//默认采用的是最小堆实现的
PriorityQueue<Integer> queue2 = new PriorityQueue<>(10);
queue2.offer(11);
queue2.offer(9);
len = queue2.size();
for (int i = 0; i < len; i++) {
System.out.println(queue2.poll());
}
//输出 9, 11
}
五、使用Java数组实现队列的简单示例
下面是使用Java数组实现队列的简单示例代码:
public class ArrayQueue {
private int[] queue; // 内部数组
private int front; // 队列头部指针
private int rear; // 队列尾部指针
private int size; // 队列当前元素个数
private int capacity; // 队列容量
public ArrayQueue(int capacity) {
this.capacity = capacity;
queue = new int[capacity];
front = 0;
rear = -1;
size = 0;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
public int size() {
return size;
}
public void enqueue(int item) {
if (isFull()) {
System.out.println("Queue is full. Cannot enqueue.");
return;
}
rear = (rear + 1) % capacity; // 循环队列,计算新的尾部位置
queue[rear] = item;
size++;
System.out.println("Enqueued: " + item);
}
public int dequeue() {
if (isEmpty()) {
System.out.println("Queue is empty. Cannot dequeue.");
return -1;
}
int item = queue[front];
front = (front + 1) % capacity; // 循环队列,计算新的头部位置
size--;
System.out.println("Dequeued: " + item);
return item;
}
public int peek() {
if (isEmpty()) {
System.out.println("Queue is empty.");
return -1;
}
return queue[front];
}
public void display() {
if (isEmpty()) {
System.out.println("Queue is empty.");
return;
}
System.out.print("Queue: ");
int index = front;
for (int i = 0; i < size; i++) {
System.out.print(queue[index] + " ");
index = (index + 1) % capacity; // 循环遍历队列
}
System.out.println();
}
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(5);
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
queue.display(); // Queue: 10 20 30
queue.dequeue();
queue.display(); // Queue: 20 30
queue.enqueue(40);
queue.enqueue(50);
queue.display(); // Queue: 20 30 40 50
queue.dequeue();
queue.dequeue();
queue.display(); // Queue: 40 50
}
}
六、队列的应用场景
1.消息队列
用于实现系统间的异步通信,可以将消息发送到队列中,然后由消费者从队列中取出进行处理。
示例:使用RabbitMQ、Kafka等消息队列实现订单处理系统,将订单消息发送到队列中,后台系统从队列中消费并处理订单。
2.线程池任务调度
用于按照顺序执行任务,通常使用队列来存储待执行的任务。
示例:使用Java的Executor框架创建线程池,将需要执行的任务添加到线程池的任务队列中,线程池按照队列中的顺序依次执行任务。
3.缓存淘汰策略
用于限制缓存的大小,当缓存满时,通过队列中的先进先出规则来淘汰最早添加的元素。
示例:使用LRU(最近最少使用)缓存淘汰算法,将不经常访问的数据移出缓存,保留最近访问的数据在缓存中。
4.网络请求调度
用于处理请求队列,按照先到先处理的顺序处理请求,实现请求的有序处理。
示例:Web服务器接收到多个客户端请求时,将请求放入请求队列,然后按照队列中的顺序依次处理请求。
5.广度优先搜索(BFS)
用于解决图和树等数据结构的搜索问题,通过队列来实现搜索的层级遍历。
示例:在无权图中找到两个节点之间的最短路径,使用广度优先搜索算法,使用队列来实现节点的层级遍历。
这些只是队列应用的一小部分示例,队列作为一种重要的数据结构,在计算机科学中被广泛应用。具体的应用场景根据问题的需求和实际情况选择合适的方法和数据结构。