队列 Queue 一个方向进,一个方向出
Queue队列提供的核心方法:
入队列:offer add
出队列:poll remove
取队首元素: peek element
前面一列发生错误是返回null 后面一列发生错误时抛出异常
Queue是否能够使用isEmpty()/size 等这样的方法呢?
答案:是可以的,因为Queue接口继承自Collection接口,而Collection接口实现了这一系列方法。


因此,判定队列是否为空也就有了两种表示方法:
java
//判定队列是否为空
if(queue.peek()==null){
}
if(queue.isEmpty()){
}
一、模拟实现队列
java
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就应该指向null
if(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());
}
}
二、数组实现队列
java
//这是一个基于数组的队列
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引用,导致内存的利用率更加低。
实现代码:
java
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(){
//如果队列为空,直接返回null
if(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 相当于队列
双端队列的实现:
java
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
代码段:
java
// 通过两个队列实现栈.
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.判定空,确保两个栈都不空,此时整体为空
代码段:
java
// 使用两个栈, 模拟实现队列.
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();
*/