文章目录
队列 Queue
前言 : 上文我们学习了栈, 知道了栈是先进后出的一种结构,并且我们使用数组实现了我们自己的栈 (顺序实现) ,并且还说过可以使用链表来实现(头插来完成栈的入, 出同理也是从头出,此时就是链式实现) , 下面我们就来学习下我们的队列
1. 队列的概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表 ,
队列具有先进先出FIFO(FirstIn First Out)
入队列:进行插入操作的一端称为队尾(Tail/Rear)
出队列:进行删除操作的一端称为队头(Head/Front)
例子 :
这里就好比我们排队打饭, 我们肯定是先到的人可以先打到饭, 而后来的后面才能打到饭, 你别说插队啥的.
这里我们知道了 队列是先进先出的数据结构, 那么我们来想一想我们可以通过我们目前所学过的数据结构来实现队列呢?
1.顺序表(ArrayList)
2.链表(LinkedList)
注意: 使用双向链表实现队列并不一定是 head进 ,tail 出, tail进 head 出也是行的,另外我们的单链表加上一个tail尾指向尾巴也是可以实现队列的
下面我们就来学习一下队列的方法
2. 队列中的方法
这里我们点击Queue中 (ctr + 鼠标左键) 然后 alt + 7
就可以看到他实现的方法了。
方法 | 功能 |
---|---|
boolean add(E e) | 入队列 |
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
E peek() | 获取队头元素 |
int size() 注意:父类的方法 | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
E remove() | 出队列 |
可以发现我们的 add 和 offer 都是入队列, 这里我们可以点入 Quueue 找到 add 方法和 offer 方法, 可以看上面的注释,这里我们就来解释一下
offer 和 add 区别 :
如果队列有大小限制, 我们想要再一个满的队列添加一个元素,add
方法 会抛出一个 unchecked异常
, 而 offer
方法则不会抛出异常而是直接返回 false
那么 队列有固定大小时可以使用 add
更好一点, 队列没有那么一般使用 offer
poll 与 remove 区别
这里 poll 和 remove
两个方法都是从队列出一个元素, 此时我们队列为空 使用remove
方法出元素的时候会抛出一个异常 , 而 poll 方法再队列为空出元素那么就不会抛出异常而是直接返回一个null
peek 与 element 区别
这两个就与 poll 和 remove 相识, 我们的 peek 与 element 是用来查看队列的头部元素, 这里我们如果队列为空使用 peek 会返回一个null 而使用 element就会抛出一个异常
这里我们的队列的方法可以自己使用一下这里就不演示使用这些方法,下面就来实现一下我们的队列。
3. 队列的实现
这里我们队列的实现 ,使用单链表和 双向链表都实现一遍
3.1 单向链表实现队列
package T_J.Test_10_15.Test;
class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
/**
* 使用单向链表实现我们的队列
*/
public class MyQueue {
// 头
public ListNode head;
// 尾
public ListNode tail;
// size 记录当前队列元素个数
public int size = 0;
/**
* 新增元素
*
* @param val
*/
public void offer(int val) {
ListNode cur = new ListNode(val);
if (head == null) {
//此时说明没有元素
head = cur;
tail = cur; // 别忘记了 尾巴也是空的 也需要赋值
size++;
return;
}
// 此时 head != null 说明队列中有元素
tail.next = cur;
tail = cur;
size++;
}
/**
* 出 队列
*/
public int poll() {
if (head == null) {
throw new RuntimeException("队列为空"); // 自定义异常
}
int oldVal = head.val;
// 注意 : 如果我们的队列只有一个元素的话出了队列,需要将 tail置空
head = head.next;
if(head == null){
tail = null;
}
size--;
return oldVal;
}
/**
* 查看队头元素
*/
public int peek() {
// 1.先判断队列是否为空
if(isEmpty()){
throw new RuntimeException("队列为空");
}
return head.val;
}
/**
* 判断队列是否为空
*/
public boolean isEmpty(){
return this.head == null;
}
/**
* 返回队列元素个数
*/
public int size(){
return this.size;
}
@Override
public String toString() {
return "MyQueue{" +
"head=" + head +
", tail=" + tail +
", size=" + size +
'}';
}
}
单向链表实现完了 , 下面使用双向链表,几乎一毛一样。
3.2 双向链表实现队列
package T_J.Test_10_15.Test;
class ListNode2 {
public int val;
public ListNode2 next;
public ListNode2 prev;
public ListNode2(int val) {
this.val = val;
}
}
public class MyQueue2 {
// 标记头
private ListNode2 head;
// 标记尾
private ListNode2 tail;
// size 记录元素个数
private int size = 0;
/**
* offer 新增操作
*/
public void offer(int val) {
ListNode2 cur = new ListNode2(val);
if (head == null) {
head = cur;
tail = cur;
size++;
return;
}
tail.next = cur;
cur.prev = tail;
size++;
tail = cur;
}
/**
* 出队列
*/
public int poll() {
if (isEmpty()) {
throw new RuntimeException("队列为空!");
}
int oldVal = head.val;
head = head.next;
if (head == null) {
// 队列中只有一个元素
tail = null;
} else {
head.prev = null;
}
size--;
return oldVal;
}
/**
* 判断当前队列是否为空
*/
public boolean isEmpty() {
return this.head == null;
// 这里定义了 size 也可以 return this.size == 0;
}
/**
* 查看队头元素
*/
public int peek(){
if(isEmpty()){
throw new RuntimeException("队列为空");
}
return head.val;
}
/**
* 返回元素个数
* @return
*/
public int size(){
return this.size;
}
@Override
public String toString() {
return "MyQueue2{" +
"head=" + head +
", tail=" + tail +
", size=" + size +
'}';
}
}
双向链表实现队列也实现完成了, 此时我就来找茬 ,我就是想要使用数组来实现队列,你能帮我实现一下吗 ?
4. 引出循环队列
5. 循环队列
方法三 : 使用标志位判断循环队列是否满了
注意: 偷个懒用之前的图
这里我们循环队列就知道了个大概那么我们就可以写一个题来更加深刻的了解他
设计循环队列
public class MyCircularQueue {
// 初始化
public MyCircularQueue(int k) {
}
// 新增元素
public boolean enQueue(int value) {
}
// 删除元素
public boolean deQueue() {
}
// 获取队首元素
public int Front() {
}
//获取队尾元素
public int Rear() {
}
// 判断队列为空
public boolean isEmpty() {
}
// 判断队列是否为满
public boolean isFull() {
}
}
5.1 使用 size判断循环队列是否满了
这里我们先来使用 size 来判断满和判断空,使用这种是最简单的。
1.初始化
2. 判断循环队列是否为满,或为空
3. 新增元素
4.删除元素
5.返回队首元素
6.获取队尾元素
这里我们就写完了 ,那么我们就可以提交看看
完美 ,这里附上代码
public class MyCircularQueue {
//记入循环队列的元素个数
private int size;
// 用来循环的数组数组
private int[] arr;
// 尾
private int rear;
// 头
private int front;
// 初始化
public MyCircularQueue(int k) {
this.arr = new int[k];
this.rear = 0;
this.front = 0;
this.size = 0;
}
// 新增元素
public boolean enQueue(int value) {
if (isFull()) {
//如果循环队列满了返回false
return false;
}
// 判断 我们的rear是否越界
if (rear >= arr.length) {
// 将 rear 放到 0下标
rear = 0;
}
//开始添加元素
arr[rear++] = value;
size++;
return true;
}
// 删除元素
public boolean deQueue() {
//判断循环队列是否为空
if (isEmpty()) {
//为空返回false
return false;
}
// 判断 front 是否越界
if (front >= arr.length) {
//将 front放到 0下标
front = 0;
}
// 此时 让 front++
front++;
size--;
return true;
}
// 获取队首元素
public int Front() {
if (isEmpty()) {
return -1;
}
int ret = front;
// 获取队首元素 ,
if (front >= arr.length) {
// 此时越界了需要将 front 变为 0 下标
ret = 0;
}
return arr[ret];
}
//获取队尾元素
public int Rear() {
if (isEmpty()) {
return -1;
}
// 我们返回 rear -1 的位置即可, 如果 rear == arr.length, 会在 新增加元素的地方置为0 ,
//所以即便 rear == arr.length ,直接返回 rear - 1即可
return arr[rear - 1];
}
// 判断队列为空
public boolean isEmpty() {
return this.size == 0;
}
// 判断队列是否为满
public boolean isFull() {
return this.size == arr.length;
}
}
那么我们来使用第二种方法再写一次 , 浪费一个位置来判断元素是否满了。
5.2 浪费一个格子判断循环队列是否满了
这里我们只需要再上面的基础上更改即可, 这里主要改的就是我们的判断满和空的方法
1.判断循环队列是否满了
2.判断循环队列是否为空
最后我们提交
这附上代码 :
package T_J.Test_10_15.Test;
public class MyCircularQueue2 {
// 用来循环的数组数组
private int[] arr;
// 尾
private int rear;
// 头
private int front;
// 初始化
public MyCircularQueue2(int k) {
this.arr = new int[k + 1];
// 这里我们要浪费一个格子,那么我们就需要再开辟数组大小的时候多开辟一个
this.rear = 0;
this.front = 0;
// this.size = 0;
}
// 新增元素
public boolean enQueue(int value) {
if (isFull()) {
//如果循环队列满了返回false
return false;
}
// 判断 我们的rear是否越界
if (rear >= arr.length) {
// 将 rear 放到 0下标
rear = 0;
}
//开始添加元素
arr[rear++] = value;
return true;
}
// 删除元素
public boolean deQueue() {
//判断循环队列是否为空
if (isEmpty()) {
//为空返回false
return false;
}
// 判断 front 是否越界
if (front >= arr.length) {
//将 front放到 0下标
front = 0;
}
// 此时 让 front++
front++;
return true;
}
// 获取队首元素
public int Front() {
if (isEmpty()) {
return -1;
}
int ret = front;
// 获取队首元素 ,
if (front >= arr.length) {
// 此时越界了需要将 front 变为 0 下标
ret = 0;
}
return arr[ret];
}
//获取队尾元素
public int Rear() {
if (isEmpty()) {
return -1;
}
// 我们返回 rear -1 的位置即可, 如果 rear == arr.length, 会在 新增加元素的地方置为0 ,
//所以即便 rear == arr.length ,直接返回 rear - 1即可
return arr[rear - 1];
}
// 判断队列为空
public boolean isEmpty() {
// 这里我判断循环队列是否为空只要判断当前的 front 和 rear是否相等即可
return rear == front;
}
// 判断队列是否为满
public boolean isFull() {
int ret = rear + 1;
if(rear + 1 >= arr.length){
ret = 0;
}
return ret == front;
// 这里也可以直接 写成
// return (rear+1) % arr.length == front
}
public static void main(String[] args) {
MyCircularQueue2 myCircularQueue2 = new MyCircularQueue2(3);
System.out.println(myCircularQueue2.enQueue(4));
System.out.println(myCircularQueue2.enQueue(2));
System.out.println(myCircularQueue2.enQueue(2));
System.out.println(myCircularQueue2.enQueue(3));
}
}
注意:如果使用这种方法写的循环队列, 再 判断满的 方法里使用了 rear + 1 >= arr.length , 会跑不过我也不知道为什么,再idea上测试的没问题,但到力扣上执行就会有问题, 那么我们就直接使用 return (rear + 1) % arr.length == front 即可。
5.3 使用标志位
最后一种也是换汤不换要的,这里就不在细谈, 这里主要的还是判断满的方法,和判断空的方法,这里就来看看
主要逻辑 就是使用 一个 boolean变量 flag ,没入一个元素 flag 设为 true , 没出一个元素 flag 设为 false , 判断满的情况就是 rear 与 front 相遇 时 false 是否为true , 判空 rear 与 front 相遇 flag 是否为false
public class MyCircularQueue {
// 使用 flag来判断当前循环数组是否满了, 没放一个元素 flag = true ,没出一个元素 flag = false
private boolean flag;
// 用来循环的数组数组
private int[] arr;
// 尾
private int rear;
// 头
private int front;
// 初始化
public MyCircularQueue(int k) {
this.arr = new int[k];
this.rear = 0;
this.front = 0;
this.flag = false;
}
// 新增元素
public boolean enQueue(int value) {
if (isFull()) {
//如果循环队列满了返回false
return false;
}
// 判断 我们的rear是否越界
if (rear >= arr.length) {
// 将 rear 放到 0下标
rear = 0;
}
//开始添加元素
arr[rear++] = value;
flag = true;
return true;
}
// 删除元素
public boolean deQueue() {
//判断循环队列是否为空
if (isEmpty()) {
//为空返回false
return false;
}
// 判断 front 是否越界
if (front >= arr.length) {
//将 front放到 0下标
front = 0;
}
// 此时 让 front++
front++;
flag = false;
return true;
}
// 获取队首元素
public int Front() {
if (isEmpty()) {
return -1;
}
int ret = front;
// 获取队首元素 ,
if (front >= arr.length) {
// 此时越界了需要将 front 变为 0 下标
ret = 0;
}
return arr[ret];
}
//获取队尾元素
public int Rear() {
if (isEmpty()) {
return -1;
}
// 我们返回 rear -1 的位置即可, 如果 rear == arr.length, 会在 新增加元素的地方置为0 ,
//所以即便 rear == arr.length ,直接返回 rear - 1即可
return arr[rear - 1];
}
// 判断队列为空
public boolean isEmpty() {
if(rear == front && flag == false){
return true;
}
return false;
}
// 判断队列是否为满
public boolean isFull() {
// 这里我们判断循环队列满需要考虑 rear == arr.length的情况,这里就需要让 创建一个变量 去 等于 0下标
int ret = rear;
if(rear == arr.length){
ret = 0;
}
if(ret == front && flag == true){
return true;
}
return false;
}
}
此时我们的 三种 方式设计循环队列就完成了 , 下面我们就来了解一下我们的双端队列 Deque
6. 双端队列
最后我们来完成几道例题 就结束我们队列的学习
7. 两道练习题
7.1 用队列实现栈
补充 : 我们将 queue1 中的 2出队列后 queue1 为空,那么我们下次添加元素就需要再 queue2中添加元素,然后出院时就需要将元素出道 queue1 中即可
代码实现 :
import java.util.LinkedList;
import java.util.Queue;
public class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
// 初始化队列
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
// 规定两个队列为空 入 queue1
if (!queue1.isEmpty()) {
queue1.offer(x);
} else if (!queue2.isEmpty()) {
queue2.offer(x);
} else {
//此时 queue1 和 queue2都为空 将元素添加到queue1
queue1.offer(x);
}
}
public int pop() {
// 出栈 , 将 size - 1 出道空的队列即可
if (empty()) {
return -1;
}
// 找 那个栈不为空,然后出元素即可 --> 封装一个方法
if (!queue1.isEmpty()) {
// 此时queue1中有元素
return popSize(queue1, queue2);
}
// 此时 queue2中有元素
return popSize(queue2, queue1);
}
public int popSize(Queue<Integer> queue1, Queue<Integer> queue2) {
// 默认 queue1 是出队列的, queue2 是接收的队列
int size = queue1.size();
for (int i = 0; i < size - 1; i++) {
// 这里只需要将 size - 1的元素出到 queue2中即可
queue2.offer(queue1.poll());
}
// 最后 queue1中的元素就是我们需要弹出的元素
return queue1.poll();
}
public int top() {
// 查看栈顶元素同样是需要 将有元素的队列移动到没有元素的队列
if (empty()) {
//栈为空直接返回 -1
return -1;
}
// 找队列不为空的调用方法
if(!queue1.isEmpty()){
return peekSize(queue1,queue2);
}
return peekSize(queue2,queue1);
}
public int peekSize(Queue<Integer> queue1, Queue<Integer> queue2) {
int size = queue1.size();
// 此时就需要全部移动
int ret = 0;
for (int i = 0; i < size; i++) {
ret = queue1.poll();
queue2.offer(ret);
}
return ret;
}
public boolean empty() {
// 判断 栈是否为空
return queue1.isEmpty() && queue2.isEmpty();
}
}
7.2 用栈实现队列
代码实现:
import java.util.Stack;
public class MyQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
// 初始化
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
// 添加元素
public void push(int x) {
// 这里无脑入 stack1即可
stack1.push(x);
}
public int pop() {
// 出栈就需要考虑 stack2是否为空了,为空就需要将stack1的元素移动到stack2
if (!stack2.isEmpty()) {
return stack2.pop();
} else {
// stack2为空开始移动元素
int size = stack1.size();
for (int i = 0; i < size; i++) {
stack2.push(stack1.pop());
}
//最后出stack2的栈顶即可
return stack2.pop();
}
}
// 查看队首元素,
public int peek() {
// 这里同理 stack2 不为空直接返回即可,为空 将stack1里的元素移动到 stack2中
if (!stack2.isEmpty()) {
return stack2.peek();
} else {
//stack2为空
int size = stack1.size();
for (int i = 0; i < size; i++) {
stack2.push(stack1.pop());
}
return stack2.peek();
}
}
public boolean empty() {
//判断模拟的队列是否为空
return stack1.empty() && stack2.empty();
}
}
本文完 , 下文预告二叉树 (难度可能会上一个档次不要怕,慢慢啃肯定是能学会的,加油)