当年面试问的最多的那批 Java 集合框架现在问的还多么? 编程十年(手动狗头) , 我把这些集合框架做成盲盒, 无论你多难, 我都想去了解你, 今天来看-----pia~, Queue,一个接口,代表着一种先进先出(FIFO)的数据结构,适用于存储元素并管理它们的顺序。常用的实现类包括LinkedList和PriorityQueue。它支持元素的插入、删除和检索操作,是在多线程环境下进行安全操作的重要工具。
一. 概念
队列: 只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(FirstIn First Out)
入队列: 进行插入操作的一端称为队(Tail/Rear)
出队列: 进行删除操作的一端称为队头(Head/Front)
二. 队列的使用
在Java中,Queue是个接口,底层是通过链表实现的。
注意: Queue是个接口,在实例化时必须实例化子类对象。
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();
q.offer(1);
q.offer(2);
q.offer(3);
q.offer(4);
q.offer(5); // 从队尾入队列
System.out.println(q.size());
System.out.println(q.peek()); // 获取队头元素
q.poll();
System.out.println(q.poll()); // 从队头出队列,并将删除的元素返回
if(q.isEmpty()){
System.out.println("队列空");
}else{
System.out.println(q.size());
}
}
三. 队列模拟实现
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有两种:顺序结构 和 链式结构。同学们思考下:队列的实现使用顺序结构还是链式结构好?
链式结构更好: 因为链式结构可以记录队头和队尾, 入队和出队的时间复杂度都是 O(1), 而 对于顺序结构的话出队时需要挪动元素, 时间复杂度为 O (N)
class Queue {
// 双向链表节点
public static class ListNode{
ListNode next;
ListNode prev;
int value;
ListNode(int value){
this.value = value;
}
}
ListNode first; // 队头
ListNode last; // 队尾
int size = 0;
// 入队列---向双向链表位置插入新节点
public void offer(int e){
ListNode newNode = new ListNode(e);
if(first == null){
first = newNode;
// last = newNode;
}else{
last.next = newNode;
newNode.prev = last;
// last = newNode;
}
last = newNode;
size++;
}
// 出队列---将双向链表第一个节点删除掉
public int poll() throws Exception {
// 1. 队列为空
// 2. 队列中只有一个元素----链表中只有一个节点---直接删除
// 3. 队列中有多个元素---链表中有多个节点----将第一个节点删除
int value = 0;
if(first == null){
throw new Exception("队列为空");
}else if(first == last){
last = null;
first = null;
}else{
value = first.value;
first = first.next;
first.prev.next = null;
first.prev = null;
}
--size;
return value;
}
// 获取队头元素---获取链表中第一个节点的值域
public int peek() throws Exception {
if(first == null){
throw new Exception("队列为空");
}
return first.value;
}
public int size() {
return size;
}
public boolean isEmpty(){
return first == null;
}
}
链表实现队列: 需要记录队头和队尾
四. 循环队列
实际中我们有时还会使用一种队列叫循环队列。如操作系统中生产者消费者模型时可以就会使用循环队列。循环队列通常使用数组实现。
数组下标循环的小技巧 :
- 下标最后再往后(offset 小于 array.length): index = (index + offset) % array.length (offset 是要移动的步数)
- 下标最前再往前(offset 小于 array.length): index = (index + array.length - offset) % array.length (offset 是要移动的步数)
如何区分空与满 :
- 通过添加 size 属性记录
每入队一个元素 size++, 每出队一个元素 size– - 保留一个位置 (多开辟一块空间)
队空时: front == rear, 队满时 front == rear + 1 - 使用标记
每删除一个元素 flag 设置为 false, 每增加一个元素 flag 设置为 true
设计一个循环队列
class MyCircularQueue {
private int[]elem;
private int head;
private int tail;
public MyCircularQueue(int k) {
//多开一个空间用来区分队列是空还是满
this.elem=new int[k+1];
}
public boolean enQueue(int value) {
if(isFull()){
return false;
}
this.elem[tail]=value;
//tail为length-1
tail=(tail+1)%elem.length;
return true;
}
public boolean isFull(){
if((tail+1)%elem.length==head){
return true;
}
return false;
}
public boolean deQueue() {
if(isEmpty()){
return false;
}
head=(head+1)%elem.length;
return true;
}
public int Front() {
if(isEmpty()){
return -1;
}
return elem[head];
}
public int Rear() {
if(isEmpty()){
return -1;
}
//注意tail为0的情况
return elem[(tail+elem.length-1)%elem.length];
}
public boolean isEmpty() {
return head==tail;
}
}
五. 双端队列 (Deque)
双端队列(deque): 是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
Deque是一个接口,使用时必须创建LinkedList的对象。
在实际使用中,使用Deque接口是比较多的,栈和队列均可以使用该接口。
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现
六. 面试题
1. 用队列实现栈。
思路:
使用两个队列, 其中一个队列作为出栈队列, 另一个作为入栈队列, 当需要出栈时, 将入栈队列中的元素导入到出栈队列中, 除了最后一个元素,此时入栈队列就剩一个元素, 这个元素就是最新入栈的元素, 这样就能进行 peek 和 pop, 操作, 此时之前的入栈队列就变成了出栈队列, 而出栈队列变成了入栈队列 . 如此循环往复
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<Integer>();
queue2 = new LinkedList<Integer>();
}
public void push(int x) {
queue2.offer(x);
while (!queue1.isEmpty()) {
queue2.offer(queue1.poll());
}
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
2. 用栈实现队列。
思路:
将一个栈当作输入栈,用于压入push传入的数据;另一个栈当作输出栈,用于 pop 和 peek操作。
每次 pop或peek 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。
class MyQueue {
Deque<Integer> inStack;
Deque<Integer> outStack;
public MyQueue() {
inStack = new ArrayDeque<Integer>();
outStack = new ArrayDeque<Integer>();
}
public void push(int x) {
inStack.push(x);
}
public int pop() {
if (outStack.isEmpty()) {
in2out();
}
return outStack.pop();
}
public int peek() {
if (outStack.isEmpty()) {
in2out();
}
return outStack.peek();
}
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
private void in2out() {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
以上就是对 Queue 的基本讲解, 希望能帮到你 !
评论区欢迎指正 !