这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解这个题目了, 所以就将整个过程写下来了。
这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
https://space.bilibili.com/8888480?spm_id_from=333.999.0.0
顺便提供一下测试链接:循环队列:
https://leetcode.cn/problems/design-circular-queue/
1. 栈和队列的介绍与实现
栈和队列都可以使用 链表
和 数组
进行实现.
1.1 队列的逻辑实现
队列就相当于是进行排队:从后面进, 肯定是到了最前面了, 然后弹出的时候, 从前面出. (先进先出).
1.1.1 队列用链表实现
- 可以先设置一个“头指针”, “尾指针”, 在进行实现的时候, 进来一个数据, 将这个数据包装成“链表”的形式,
- 第一个数据传入的时候, 将“头指针”和“尾指针”同时指向第一个数据,
- 然后后面数据加入的时候, 移动“尾指针”, 将“尾指针”移动队列的尾部.
- 弹出一个数据的时候, 就修改“头指针”, 将“头指针”向后移动.
1.1.2 队列用数组实现
在我们做题的时候, 题目中的数据量一般都是确定的, 所以我们可以直接用数组实现, 只要是加入数据的总次数不超过 n
就可以进行设置.
比如:题目中说明给的数据量是 5000
, 那我们的数组就干脆直接设置为:arr[5000]
.
- 设置一个左边界和右边界:
l 和 r
,此时l = r = 0
. 里面有数据的条件:l < r
, 若是没有数据说明:l == r
.- 若是进入一个数据, 那么就将
r++:queue[r++] = num
, - 若是弹出一个数据, 那么就将
l++:return queue[l++]
, 将l
向r
的位置靠近.
- 若是进入一个数据, 那么就将
1.1.3 代码实例
用 数组
的方式实现队列.
// 实际刷题时更常见的写法,常数时间好
// 如果可以确定加入操作的总次数不超过n,那么可以用
// 一般笔试、面试都会有一个明确数据量,所以这是最常用的方式
public static class Queue2 {
public int[] queue;
public int l;
public int r;
// 加入操作的总次数上限是多少,一定要明确
public Queue2(int n) {
queue = new int[n];
l = 0;
r = 0;
}
// 调用任何方法之前,先调用这个方法来判断队列内是否有东西
public boolean isEmpty() {
return l == r;
}
// 插入一个数据
public void offer(int num) {
queue[r++] = num;
}
// 弹出一个数据
public int poll() {
return queue[l++];
}
// ?
// l...r-1 r // [l..r)
public int head() {
return queue[l];
}
public int tail() {
return queue[r - 1]; // 因为是 r 是数据加进来的位置, 所以头位置是 r - 1.
}
public int size() {
return r - l; // 队列的大小是“r - l”.
}
}
1.2 栈的逻辑实现
栈就像是图片上的形式, 只能从最顶处放入, 也只能从最顶处拿出. 所以栈的结构是:
后进先出
.
1.2.1 栈使用数组实现
在做题的时候, 数据量是确定的, 保证同时在栈里的元素个数不会超过 n
就可以使用
数组
实现.
- 先设置一个数组
arr[]
, 设置一个size变量:size = 0
然后放入数据- 放入数据的时候, 放到
size
的位置, 然后将size++
. - 弹出数据的时候, 弹出
size - 1
, 然后将size--
.- 而且弹出数据之后也是不需要进行清理的, 因为后续放入数据的时候会直接将原来残留的数据覆盖掉.
- 放入数据的时候, 放到
1.2.2 代码实例
// 实际刷题时更常见的写法,常数时间好
// 如果可以保证同时在栈里的元素个数不会超过n,那么可以用
// 也就是发生弹出操作之后,空间可以复用
// 一般笔试、面试都会有一个明确数据量,所以这是最常用的方式
public static class Stack2 {
public int[] stack;
public int size;
// 同时在栈里的元素个数不会超过n
public Stack2(int n) {
stack = new int[n];
size = 0;
}
// 调用任何方法之前,先调用这个方法来判断栈内是否有东西
public boolean isEmpty() {
return size == 0;
}
public void push(int num) {
stack[size++] = num;
}
public int pop() {
return stack[--size];
}
public int peek() {
return stack[size - 1];
}
public int size() {
return size;
}
}
2. 环形队列的实现
2.1 环形队列的逻辑结构
环形队列的意思就是:设置一个数组:arr[]
, 然后将数据进行放入, 假设数组的长度是:5
, 放入数据, 等到放满的时候, 将已经使用过的值弹出, 然后可以继续将数据进行放入.
那这样来看, 放入数据的时候, 只要同时在这个数组内的数据量不超过 5
, 就能一直存放数据.
2.2 代码实例
题目难点:边界的位置不好扣, 经历一系列的加入和弹出, 右边移动一个位置, 左边移动一个位置, 这样就会麻烦, 头尾一直在不断的相互追赶, 这就会非常的麻烦, 所以需要一个简单的方法, 比如引入一个 变量
, 将头尾进行“解耦”.
解释:
- 首先和正常队列一样设置
l 和 r, l = r = 0
, 继续设置一个size = 0
,size
表示队列的大小,limit 是对应的队列的限制(最大的长度)
. - 开始对队列进行操作, (并且要判断其正确性)
- 若是将数据放入队列(放入尾部):
r++, 此时队列也扩大的, 所以:size++
. 若是到了边界:limit - 1 就返回 0
, 只要满足size < limit
就一定正确. (因为这说明这个队列中还有位置). - 若是将数据弹出队列(弹出头部):
l++, 此时队列缩小, 所以:size--
. 若是到了边界:limit - 1 就返回 0
只要满足size > 0
一定正确. (因为说明这个队列存在可以弹出的数据). - 这样就利用了
size
这个变量实现了“头尾”的解耦性. 不用管“头尾”相互追了.
- 若是将数据放入队列(放入尾部):
这个方法的实现, 自己随便用几个数据, 然后实现下面代码中的几个主要方法就行了, 尝试一遍就会明白如何利用 size
实现了“头尾”的解耦性
MyCircularQueue {
public int[] queue;
public int l, r, size, limit;
// 同时在队列里的数字个数,不要超过k
public MyCircularQueue(int k) {
queue = new int[k];
l = r = size = 0;
limit = k;
}
// 如果队列满了,什么也不做,返回false
// 如果队列没满,加入value,返回true
public boolean enQueue(int value) {
if (isFull()) {
return false;
} else {
queue[r] = value;
// r++, 结束了,跳回0
r = r == limit - 1 ? 0 : (r + 1);
size++;
return true;
}
}
// 如果队列空了,什么也不做,返回false
// 如果队列没空,弹出头部的数字,返回true
public boolean deQueue() {
if (isEmpty()) {
return false;
} else {
// l++, 结束了,跳回0
l = l == limit - 1 ? 0 : (l + 1);
size--;
return true;
}
}
// 返回队列头部的数字(不弹出),如果没有数返回-1
public int Front() {
if (isEmpty()) {
return -1;
} else {
return queue[l];
}
}
public int Rear() {
if (isEmpty()) {
return -1;
} else { // 看原来的位置在哪里? 若是 r != 0, 那就是 r - 1 位置是尾部,
int last = r == 0 ? (limit - 1) : (r - 1); //若是 r == 0, 那就是 limit - 1 位置
return queue[last];
}
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == limit;
}
}