1.队列的基本概念?
队列:只允许在一端进行插入操作,在另一端进行删除操作的特殊线性表.
通常,入数据的一端称为队尾,出数据的一端称为队头.类似于"排队",要先入先出(FIFO).也就是最先进去的元素最先出来.
❤队列的底层是数组(顺序)或者链表(链式).
一般循环队列用数组实现,我们之后讲解循环队列.
2.队列的使用
首先,我们要知道,Queue是一个接口,底层通过链表实现.
2.1 队列的一些主要方法
2.1 单链表实现队列
😎Queue是一个接口,我们不能直接实例化一个接口.但我们可以用Queue接口实例化LinkedList对象,因为LinkedList对象实现了Queue接口.
public class TestQueue {
public static void main(String[] args) {
Queue<Integer> queue=new LinkedList<>();//LinkedList对Queue中的抽象方法进行了重写
queue.offer(1);
queue.offer(2);
queue.offer(3);//入队顺序是1,2,3,那么出队顺序也是1,2,3
System.out.println(queue.peek());
System.out.println("peek操作后的队首元素是"+queue.peek());
queue.poll();
System.out.println("pop操作后的队首元素是"+queue.peek());
}
}
结果:
😛接下来,我们用单链表来实现一个队列.
public interface MyQueue<E> {
void offer(E val);//因为是一个接口.所以默认是public abstract修饰的
E pop();
E peek();
boolean isEmpty();
int size();
}
public class LinkedQueue<E> implements MyQueue<E> {
private Node head;//头结点
private Node tail;//尾结点
private int size;//存储的元素个数
private class Node{//内部类
E val;
Node next;
Node(E val){
this.val=val;
}
}
public String toString(){
StringBuilder sb=new StringBuilder();
sb.append("head [");
for(Node x=head;x!=null;x=x.next){
sb.append(x.val);
if(x.next!=null){
sb.append("->");
}
}
sb.append("]");
return sb.toString();
}
@Override
public void offer(E val) {
Node node=new Node(val);
if(isEmpty()){
head=node;
tail=node;
}else {
head.next=node;
tail=node;
}
size++;
}
@Override
public E pop() {
if (!isEmpty()){
Node node=head;//记得先记录一下
head=head.next;
size--;
return node.val;
}
return null;
}
@Override
public E peek() {
if (!isEmpty()){
return head.val;
}
throw new IllegalArgumentException("队列为空,获取非法");
}
@Override
public boolean isEmpty() {
if(size==0){
return true;
}
return false;
}
@Override
public int size() {
return size;
}
}
😜上面这个队列是用单链表实现的,它的入队和出队的时间复杂度都是O(1).这是因为我们还设置了一个指向尾部的结点last.
🤨我们还要保证==尾插入队,头删出队.==这是为什么呢?
是因为单链表只有后继,没有前驱(也就是只知道后面是谁,但是不知道前面是谁).
🤨假如我们要尾删出队,那我们就需要找到被删除结点的前一个结点是谁,时间复杂度就变成O(N)了.
头删的话,我们只需要改变头结点的指向就好了,时间复杂度也就是O(1).
尾插的话,我们只需要让尾结点指向新的结点就可以了,时间复杂度也是O(1).
3.循环队列(环形队列/环形缓冲器)
🤩那我们先来了解什么是循环队列(Circle Queue)?
循环队列是一种线性存储结构,它用数组实现.它就是把顺序队列的首尾相连,从逻辑上看是个环,它的大小是固定的…那么我们就要想想增加删除元素该怎么操作?下标是如何变化的.
❓那么就有另一个问题了,为什么要引入循环队列呢?它有什么作用呢?
循环队列最大的作用就是解决伪溢出(假溢出)的发生.
😘我们先来看一下普通线性队列的下标是如何变化的.
我们定义两个引用,分别指向队列头部(front)和尾部(rear).
在队列中没有元素的时候,front和rear都指向下标为0的位置.
当我们向队列中放入一个元素时,指向队列尾部的引用rear就改变了自己的指向.
随着元素的不断增加,队列就满了.分别指向队列头部和尾部的引用它们的位置就变成下图所示了.
此时,我们让队列出几个元素.变成下图所示.
😎注意:我们发现,此时,rear的指向告诉我们这个队列已经满了,不可以再往里面放入元素了,但是我们发现front引用之前是有空间的,队列并没有满,这个就是伪溢出.这会造成空间资源的浪费.
所以呢😍我们的解决方法就是引入循环队列.
我们来看循环队列的结构.(注意:只是逻辑上是这个形状的,并不存在物理上是这样的空间.)
此时,队列对空,front和rear指向同一个位置.
此时,队列满了,front和rear也指向同一个位置.
那么,我们的数组下标是如何实现循环变化的呢?
😊答案是这样的:对front和rear进行取模操作
rear=(rear+1)%数组的长度
front=(front+1)%数组的长度
❓那么,就产生一个问题:front和rear指向同一个位置的时候,队列到底是空还是满?
😘解决这个问题有三种方法:
1.设置一个属性size,去记录此时队列中元素的个数
2.牺牲一个空间去表示队列已满.
3.这是一个标记flag
我们来对第二个方法做个补充:
为空时->front == rear
为满时->front==(rear+1)%数组的长度
总结:循环队列的好处就是我们可以利用之前用过的空间,在普通队列中,一旦满了就不能再擦入元素了,即使前面还有元素.
代码:
public class MyCircularQueue {
public int[] elem;
public int front;
public int rear;
public MyCircularQueue(int k) {//构造器,设置队列长度为 k
//如果采用浪费空间的方法,这里必须多加一个1
elem=new int[k+1];
}
public boolean enQueue(int value) {//向循环队列插入一个元素。如果成功插入则返回真
if (isFull()){
return false;
}
elem[rear]=value;
rear=(rear+1)%elem.length;//注意:不能写成rear++
return true;
}
public boolean deQueue() {//从循环队列中删除一个元素。如果成功删除则返回真
if (isEmpty()){
return false;
}
front=(front+1)%elem.length;//也不能写成front++
return true;
}
public int Front() {//从队首获取元素。如果队列为空,返回 -1
if (isEmpty()){
return -1;
}
return elem[front];
}
public int Rear() {//获取队尾元素。如果队列为空,返回 -1
if (isEmpty()){
return -1;
}
//return elem[rear-1];//要注意一种特殊情况,就是当rear=0的时候.
int index=(rear==0?elem.length-1:rear-1);
return elem[index];
}
public boolean isEmpty() {//检查循环队列是否为空
if(front==rear){
return true;
}
return false;
}
public boolean isFull() {//检查循环队列是否已满
//此时我们用牺牲一个空间的方法
if((rear+1)%elem.length==front){
return true;
}
return false;
}
}
4.双端队列(Deque)
双端队列是指两端都允许进行入队和出队操作的队列.元素可以从对头出队和入队,也可以从队尾出队和入队.Deque就是double ended queue.
4.1 双端队列的实现
Deque是一个接口,它是不能直接实现的,使用时必须创建LinkedList或者ArrayDeque实例.
😊栈和队列均可以使用该接口.
public static void main(String[] args) {
Deque<Integer> queue=new LinkedList<>();//双端队列的链式实现
Deque<Integer> stack=new ArrayDeque<>();//双端队列的线性实现
}
😀双端队列的顺序实现
public static void main(String[] args) {
//可以使用双端队列作为栈使用
Deque<Integer> stack=new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.peek());
System.out.println(stack.pop());
}
😀双端队列的链式实现
public static void main(String[] args) {
//可以使用双端队列作为栈使用
Deque<Integer> stack=new LinkedList<>();
stack.offer(1);
stack.offer(2);
stack.offer(3);
System.out.println(stack.poll());//获取第一个元素
System.out.println(stack.pop());
}
5.力扣例题
方法一:用两个队列去实现栈.
👌入栈:要入到非空的队列
👌出栈:要出非空的队列,出size-1(也就是非空这个队列的长度-1)个元素到另一个队列中
核心的三个功能是入栈,出栈,获取栈顶元素:
😀入栈:定义两个队列,入栈要入到空的那个队列里.
😀出栈:要出不为空的队列,把里面size-1个元素移到另一个队列中,然后此时那个原本不为空的队列剩下的那一个元素就是栈顶要出的那个元素.
😀获取栈顶元素:把不为空的队列里的元素移到另一个队列中,最后移动的那个元素就是要获取的栈顶元素.
代码:
public class MyStack {
private Queue<Integer> queue1;
private Queue<Integer> queue2;
public MyStack() {
queue1=new LinkedList<>();
queue2=new LinkedList<>();
}
public void push(int x) {//将元素 x 压入栈顶
//入栈要入到不为空的队列中
if (!queue1.isEmpty()){
queue1.offer(x);
}else if(!queue2.isEmpty()){
queue2.offer(x);
}else {//都为空,也就是向栈中放入第一个元素的时候,默认放到第一个队列中
queue1.offer(x);
}
}
public int pop() {//移除并返回栈顶元素
//要出不为空的队列,出size-1个元素到另一个队列中
if (empty()){//两个队列都为空,意味着栈为空
return -1;
}
if (!queue1.isEmpty()){
int size=queue1.size();
for(int i=0;i<size-1;i++){//这一步不写成i<queue1.size-1是因为,queue1.size会变.
int val=queue1.poll();
queue2.offer(val);
}
return queue1.poll();
}else {
int size=queue2.size();
for(int i=0;i<size-1;i++){
int val=queue2.poll();
queue1.offer(val);
}
return queue2.poll();
}
}
public int top() {//返回栈顶元素
if (empty()){//两个队列都为空,意味着栈为空
return -1;
}
if (!queue1.isEmpty()){
int size=queue1.size();
int val=-1;
for(int i=0;i<size;i++){
val=queue1.poll();
queue2.offer(val);
}
return val;
}else {
int size=queue2.size();
int val=-1;
for(int i=0;i<size;i++){
val=queue2.poll();
queue1.offer(val);
}
return val;
}
}
public boolean empty() {//如果栈是空的,返回 true ;否则,返回 false
if (queue1.isEmpty()&&queue2.isEmpty()){//两个队列都为空则栈为空
return true;
}
return false;
}
}
😎方法二:用一个队列实现
代码:
public class MyStack3 {
//使用一个队列模拟实现栈
Queue<Integer> queue;
public MyStack3() {
queue=new LinkedList<>();
}
public void push(int x) {
int size=queue.size();//入栈前,先获得队列中元素的个数
queue.offer(x);//将元素入队列
for(int i=0;i<size;i++){//将队列前size个元素先出队列再入队列.这样就保证了先入后出.此时队列前端的元素就是就是新入栈的元素.且队列的前端
//后端分别对应栈顶和栈底
queue.offer(queue.poll());
}
}
public int pop() {//每次入栈的操作都保证了队列前端为栈顶元素.所以直接出队列的元素得到的就是栈顶元素.top方法同理
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
if (queue.isEmpty()){
return true;
}
return false;
}
}
大体思路:定义两个栈,一个用来存储进入队列中的元素(stack1),另一个用来出队列里面的元素(stack2),也就是出队列元素的时候,直接出satck2里面的元素,如果stack2为空,就把stack1里面的元素移到stack2里.
代码:
public class MyQueue {
//大体思路:用两个栈,一个栈用来存储入队列的元素,另一个栈用来出队列中的元素.出的时候,如果用来出元素的栈为空,就把存储元素的那个栈的元素移到出元素这个栈里面.
Stack<Integer> stack1;
Stack<Integer> stack2;
public MyQueue() {
stack1=new Stack<>();
stack2=new Stack<>();
}
public void push(int x) {//将元素 x 推到队列的末尾
stack1.push(x);
}
public int pop() {//从队列的开头移除并返回元素
if (!stack2.empty()){
return stack2.pop();
}else {
while (!stack1.empty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
public int peek() {//返回队列开头的元素
if (!stack2.empty()){
return stack2.peek();
}else {
while (!stack1.empty()){
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
public boolean empty() {//如果队列为空,返回 true ;否则,返回 false
return (stack1.empty()&&stack2.empty());//如果两个栈都为空,那么队列为空
}
}