队列 Queue 一个方向进,一个方向出
Queue队列提供的核心方法:
入队列:offer add
出队列:poll remove
取队首元素: peek element
前面一列发生错误是返回null 后面一列发生错误时抛出异常
Queue是否能够使用isEmpty()/size 等这样的方法呢?
答案:是可以的,因为Queue接口继承自Collection接口,而Collection接口实现了这一系列方法。
因此,判定队列是否为空也就有了两种表示方法:
//判定队列是否为空if(queue.peek()==null){}if(queue.isEmpty()){}
一、模拟实现队列
package Queue;
//基于单链表实现的队列
public class MyQueue {//链表的一个节点public class Node{public String val;public Node next;//提供构造方法public Node(String val) {this.val = val;this.next = null;}}//把队列的头部和尾部都记录下来//基于俩表实现队列//1.入队—>尾插//2.出队—>尾删private Node head;private Node tail;//入队public void offer(String val) {Node newNode = new Node(val);//考虑特殊情况 如果链表中没有元素if(head== null){head = newNode;tail = newNode;return;}//一般情况tail.next = newNode;tail = newNode;}//出队列public String poll(){//如果链表是空的if(head == null){return null;}//一般情况//保存头部节点的值//接下来把这个节点删除后,需要返回这个值String val = head.val;head = head.next;//如果链表的节点数目超出一个,删掉一个元素,不影响tail的指向//但是,如果链表的节点数目只有一个,删掉这个元素,此时tail就应该指向nullif(head.next== null){tail = null;}return val;}//取队首元素public String peek(){//如果链表是空的if(head == null){return null;}return head.val;}//判断队列是否为空public Boolean isEmpty(){return head == null;}//计算队列的长度public int size(){int size = 0;for(Node cur = head;cur!= null;cur =cur.next){size++;}return size;}//一些测试public static void main(String[] args) {MyQueue queue = new MyQueue();queue.offer("a");queue.offer("b");queue.offer("c");queue.offer("d");queue.offer("e");System.out.println(queue.peek());System.out.println(queue.poll());System.out.println(queue.peek());System.out.println(queue.poll());}
}
二、数组实现队列
//这是一个基于数组的队列Queue<Integer> queue1 = new ArrayDeque<>();//基于数组实现的双端队列
首先先创建一个数组new int[8]
和顺序表设定类似,不是一上来这8个格子都被使用了,而是随着后续入队列逐渐使用。
由于是一个队列,使用head下标记录队首位置,使用tail下标记录队尾元素的下一个位置。
初始情况下,head 和tail都指向0位置,此时认为是一个空的队列。
队列能入也能出,出队列,得从head的位置进行考虑,如果是按照顺序表头删的做法,此时就需要搬运大量的元素,实现队列的效率就太低了。
所以这里的出队列我们选择用逻辑删除:这里的删除,不是真的把数据给改成别的,而是在逻辑上将其标记成无效。
此处也是往后移动head的位置,由于[head,tail)构成前闭后开开区间,当head++之后,之前head指向元素就被视为无效了。
当tail到达数组末尾,此时是否就意味着队列满了呢?并非。数组版本的队列,就像是把数组弯过来,头尾相接,构成了一个环。也就是“循环队列”
如果在这个循环队列中,队列满了怎么办?有人说,可以通过head 和tail是否重合来判断队列是否满了,但是最初队列为空的时候,head和tail也是重合的。
如何区分上述队列是空的还是满的呢?
此时我们有两种方案来解决这个问题:
方案一:直接浪费一个格子,当tail走到head的前一个位置的时候哦,就视为队列满了,确保再队列满的时候,tail就是head的前一个位置。队列为空的时候,tail与head才重合。
方案二:引入一个size变量即可,size ==0 就是空 size = arr.length就是满了
虽然队列是有 基于链表 和 基于数组两种风格,实际开发中,基于数组的方案是用的更多的。
数组这种方案的优点是什么呢?
1.拥有更高的效率:入队列/出货队列就是简单的head++ tail++ 执行速度更快。这时候就有人想问:“链表,不也就是修改一下引用的指向,时间复杂度也是(1)?为什么说基于数组的执行速度更快?”原因是:链表在进行访问下一个元素的时候,需要多义词间接寻址(先读取引用的值,得到了地址,再根据地址找到对应内存空间)由此处我们可以知道,效率的高与低,运行速度的快和慢都是相对的。
2.对于队列中元素个数的上限是可控的:对于链表版本来说,无限地往里面插入元素,只要你元素够,就可以插入。但是如果你的代码吹按bug了,不小心再入队列,这里就会出现死循环。此时链表版本,不会直接报错,而是把所有的内存都耗尽,导致严重的后果(比如整个程序瘫痪了)但是,如果是数组版本,并且不去自动扩容的话,如果出现类似的问题,后续就能够再入队列的时候及时报错,把问题影响范围缩小。
3.数组版本的队列,内存使用率是更高的:链表版本,由于需要保存额外的next引用,导致内存的利用率更加低。
实现代码:
package Queue;public class MyQueueByArray {//首先创建一个数组private String[] arr = null;//队首private int head = 0;//队尾private int tail = 0;//队列的元素个数private int size = 0;//来一个构造方法public MyQueueByArray(){arr = new String[1000];}//再来一个给定参数的构造方法public MyQueueByArray(int capacity){arr = new String[capacity];}//1.入队列操作public void offer(String val){//如果队列满了 直接返回if(size == arr.length){return;}//把新的元素,放到tail的位置arr[tail] = val;//更新tail的指向tail++;if(tail == arr.length){tail =0;}//更新tail的指向,其实还有另外一种写法//更推荐上面的写法,而不是这里的 % 的写法//tail=(tail+1)%arr.length;size++;}//2.出队列操作public String poll(){//如果队列为空,直接返回nullif(size ==0){return null;}//取出队首元素 保存起来 以便接下来返回值String elem = arr[head];head++;//更新head的指向并且进行判断if(head == arr.length){head = 0;}size--;return elem;}//3.查看队首元素public String peek(){if(size ==0){return null;}return arr[head];}//4.判断队列是否为空public Boolean isEmpty(){return size ==0;}//5.获取队列长度public int size(){return size;}//测试public static void main(String[] args) {MyQueueByArray myQueueByArray = new MyQueueByArray();myQueueByArray.offer("a");myQueueByArray.offer("b");myQueueByArray.offer("c");myQueueByArray.offer("d");System.out.println(myQueueByArray.peek());System.out.println(myQueueByArray.poll());System.out.println(myQueueByArray.poll());}
}
三、双端队列
双端队列,虽然是叫“队列”,但他也能当作“栈”来使用,addLast搭配removeLast,相当于栈
addFirst 搭配 removeLast 相当于队列
双端队列的实现:
public class Test2 {public static void main(String[] args) {//创建双端队列//Deque<Integer> deque = new ArrayDeque<>();Deque<Integer> deque = new LinkedList<>();//对于Queue提供的各种功能,deque也都是支持的//除此之外,Deque提供了其他的功能}
}
四、有关队列的OJ题
实现思路:
1.准备两个队列A,B
2.入栈:先把A中的所有元素往B里面倒腾(A循环出队列,把出来的元素,入队列到B中)当A中就剩最后一个元素的时候,把这个元素当作栈的元素,删除掉就可以了
当完成一次出栈,所有的元素都被倒腾到B这个队列中了,此时就可以交换A和B的指向,后续如果再需要入栈操作,还是继续往A中添加
4.取栈顶元素:和出栈类似,把A中的元素往B里倒腾,倒腾的过程中,当就剩一个元素的时候,把这个元素的值返回,接下来继续把这个值添加到B中的,然后还是交换A和B
代码段:
// 通过两个队列实现栈.
public class MyStack {private Queue<Integer> A = new LinkedList<>();private Queue<Integer> B = new LinkedList<>();public MyStack() {}private void swap() {Queue<Integer> tmp = A;A = B;B = tmp;}public void push(int x) {// 入栈的时候// 把 x 添加到队列 A 中.A.offer(x);}public int pop() {// 出栈的时候// 判定一下是否为空if (empty()) {// 为空, 直接返回.return 0;}// 把 A 中的元素往 B 里面倒腾. 直到 A 中就剩最后一个元素的时候, 这个元素就可以被删除了.// 循环结束, 就剩一个元素.while (A.size() > 1) {int n = A.poll();B.offer(n);}// 循环结束, 说明 A 中就剩一个元素了. 最后这个元素不能插入到 B 中.int ret = A.poll();// 交换 A 和 B.swap();return ret;}public int top() {if (empty()) {// OJ 题不能抛出异常. 并且也不能修改 返回值类型为 Integer 此时也无法返回 null. 只能返回 0.// 题目本身应该不会有栈为空再 top 的情况.return 0;}// 取栈顶元素, 也是把 A 的元素往 B 里倒腾.while (A.size() > 1) {int n = A.poll();B.offer(n);}// 取出最后一个元素int ret = A.poll();// 把最后一个元素添加到 B 中. (和 pop 相比, 就只是这里多了一行, 别的地方都一样) .B.offer(ret);// 交换 A 和 B.swap();return ret;}public boolean empty() {// 会交换 A 和 B. 所以 B 始终为 空的 . 抓住 A 的空就可以判定整体的 空.return A.isEmpty();}}
/*** Your MyStack object will be instantiated and called as such:* MyStack obj = new MyStack();* obj.push(x);* int param_2 = obj.pop();* int param_3 = obj.top();* boolean param_4 = obj.empty();*/
思路:创建两个栈
1.入队列:把所有B中的元素倒腾到A,往A中入栈
2.出队列:把所有A的元素倒腾给B,从B出栈
3.取队首元素:也是把A的元素倒腾给B,取B的栈顶
4.判定空,确保两个栈都不空,此时整体为空
代码段:
// 使用两个栈, 模拟实现队列.
public class MyQueue {// 创建两个栈// A 用于入队列, B 用于出队列.Stack<Integer> A = new Stack<Integer>();Stack<Integer> B = new Stack<Integer>();public void push(int x) {// 先把 B 中的所有元素倒腾到 A 里, 然后把元素添加到 A 中.while (!B.isEmpty()) {A.push(B.pop());}A.push(x);}public int pop() {// 先把 A 中的所有元素倒腾到 B 里, 然后弹出 B 栈顶元素.while (!A.isEmpty()) {B.push(A.pop());}return B.pop();}public int peek() {// 先把 A 的所有元素倒腾到 B 里, 取 B 的栈顶元素.while (!A.isEmpty()) {B.push(A.pop());}return B.peek();}public boolean empty() {return A.isEmpty() && B.isEmpty();}
}/*** Your MyQueue object will be instantiated and called as such:* MyQueue obj = new MyQueue();* obj.push(x);* int param_2 = obj.pop();* int param_3 = obj.peek();* boolean param_4 = obj.empty();*/