知识点来自队列和栈
实践为答题代码
文章目录
队列和栈
一、队列:先入先出的数据结构
1.先入先出的数据结构(FIFO)
在先入先出的数据结构中,将首先处理添加到队列中的第一个元素。
队列是典型的 FIFO 数据结构。
-
插入(insert)操作也称作入队(enqueue),新元素始终被添加在队列的末尾。
-
删除(delete)操作也被称为出队(dequeue)。 只能移除第一个元素。
2.队列-实现
为了实现队列,可以使用动态数组和指向队列头部的索引。
队列应支持两种操作:入队和出队。入队会向队列追加一个新元素,而出队会删除第一个元素。 所以我们需要一个索引来指出起点。
这是一个供你参考的实现:
#include <iostream>
class MyQueue {
private:
// store elements
vector<int> data;
// a pointer to indicate the start position
int p_start;
public:
MyQueue() {p_start = 0;}
/** Insert an element into the queue. Return true if the operation is successful. */
bool enQueue(int x) {
data.push_back(x);
return true;
}
/** Delete an element from the queue. Return true if the operation is successful. */
bool deQueue() {
if (isEmpty()) {
return false;
}
p_start++;
return true;
};
/** Get the front item from the queue. */
int Front() {
return data[p_start];
};
/** Checks whether the queue is empty or not. */
bool isEmpty() {
return p_start >= data.size();
}
};
int main() {
MyQueue q;
q.enQueue(5);
q.enQueue(3);
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
}
缺点:
上面的实现很简单,但在某些情况下效率很低。 随着起始指针的移动,浪费了越来越多的空间。
让我们考虑一种情况,即我们只能分配一个最大长度为 5 的数组。当我们只添加少于 5 个元素时,我们的解决方案很有效。 例如,调用入队函数四次后还想要将元素 10 入队,那么可以成功。
但是不能接受更多的入队请求,因为现在队列已经满了。但是如果将一个元素出队,在这种情况下,应该能够再接受一个元素。
3.循环队列
上面提供了一种简单但低效的队列实现。
更有效的方法是使用循环队列。 具体来说,可以使用固定大小的数组和两个指针来指示起始位置和结束位置。 目的是重用我们之前提到的被浪费的存储。
实践:设计循环队列
typedef int Datatype;
typedef struct {
int head,tail;
int size,Maxnum;
Datatype *data;
} MyCircularQueue;
bool myCircularQueueIsFull(MyCircularQueue* obj);
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue *p=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
p->data=(Datatype*)malloc(sizeof(Datatype)*k);
p->size=0;
p->Maxnum=k;
p->head=-1;
p->tail=-1;
return p;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj)){
return false;
}
if(obj->head==-1&&obj->tail==-1){
obj->head=0;
obj->tail=0;
obj->data[obj->tail]=value;
obj->size++;
}else if(obj->tail==obj->Maxnum-1){
obj->tail=0;
obj->data[obj->tail]=value;
obj->size++;
}else{
obj->tail++;
obj->data[obj->tail]=value;
obj->size++;
}
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)){
return false;
}
if(obj->head==obj->Maxnum-1){
obj->head=0;
obj->size--;
}else{
obj->head++;
obj->size--;
}
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)){
return -1;
}
return obj->data[obj->head];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)){
return -1;
}
return obj->data[obj->tail];
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
if(obj->size==0){
return true;
}else{
return false;
}
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
if(obj->size==obj->Maxnum){
return true;
}else{
return false;
}
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->data);
free(obj);
}
4.循环队列-实现
class MyCircularQueue {
private:
vector<int> data;
int head;
int tail;
int size;
public:
/** Initialize your data structure here. Set the size of the queue to be k. */
MyCircularQueue(int k) {
data.resize(k);
head = -1;
tail = -1;
size = k;
}
/** Insert an element into the circular queue. Return true if the operation is successful. */
bool enQueue(int value) {
if (isFull()) {
return false;
}
if (isEmpty()) {
head = 0;
}
tail = (tail + 1) % size;
data[tail] = value;
return true;
}
/** Delete an element from the circular queue. Return true if the operation is successful. */
bool deQueue() {
if (isEmpty()) {
return false;
}
if (head == tail) {
head = -1;
tail = -1;
return true;
}
head = (head + 1) % size;
return true;
}
/** Get the front item from the queue. */
int Front() {
if (isEmpty()) {
return -1;
}
return data[head];
}
/** Get the last item from the queue. */
int Rear() {
if (isEmpty()) {
return -1;
}
return data[tail];
}
/** Checks whether the circular queue is empty or not. */
bool isEmpty() {
return head == -1;
}
/** Checks whether the circular queue is full or not. */
bool isFull() {
return ((tail + 1) % size) == head;
}
};
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue obj = new MyCircularQueue(k);
* bool param_1 = obj.enQueue(value);
* bool param_2 = obj.deQueue();
* int param_3 = obj.Front();
* int param_4 = obj.Rear();
* bool param_5 = obj.isEmpty();
* bool param_6 = obj.isFull();
*/
5.队列-用法
队列有两个重要的操作,入队 enqueue 和出队 dequeue。 此外,我们应该能够获得队列中的第一个元素,因为应该首先处理它。
下面是使用内置队列库及其常见操作的一些示例:
#include <iostream>
int main() {
// 1. Initialize a queue.
queue<int> q;
// 2. Push new element.
q.push(5);
q.push(13);
q.push(8);
q.push(6);
// 3. Check if queue is empty.
if (q.empty()) {
cout << "Queue is empty!" << endl;
return 0;
}
// 4. Pop an element.
q.pop();
// 5. Get the first element.
cout << "The first element is: " << q.front() << endl;
// 6. Get the last element.
cout << "The last element is: " << q.back() << endl;
// 7. Get the size of the queue.
cout << "The size is: " << q.size() << endl;
}
二、*队列和广度优先搜索
先决条件:树的层序遍历树的遍历
广度优先搜索(BFS)是一种遍历或搜索数据结构(如树或图)的算法。
如前所述,我们可以使用 BFS 在树中执行层序遍历。
我们也可以使用 BFS 遍历图。例如,我们可以使用 BFS 找到从起始结点到目标结点的路径,特别是最短路径。
我们可以在更抽象的情景中使用 BFS 遍历所有可能的状态。在这种情况下,我们可以把状态看作是图中的结点,而以合法的过渡路径作为图中的边。
1.队列和BFS
广度优先搜索(BFS)的一个常见应用是找出从根结点到目标结点的最短路径。
- 结点的处理顺序是什么?
在第一轮中,我们处理根结点。在第二轮中,我们处理根结点旁边的结点;在第三轮中,我们处理距根结点两步的结点;等等等等。
与树的层序遍历类似,越是接近根结点的结点将越早地遍历。
如果在第 k 轮中将结点 X 添加到队列中,则根结点与 X 之间的最短路径的长度恰好是 k。也就是说,第一次找到目标结点时,你已经处于最短路径中。
- 队列的入队和出队顺序是什么?
如上面的动画所示,我们首先将根结点排入队列。然后在每一轮中,我们逐个处理已经在队列中的结点,并将所有邻居添加到队列中。值得注意的是,新添加的节点不会立即遍历,而是在下一轮中处理。
结点的处理顺序与它们添加到队列的顺序是完全相同的顺序,即先进先出(FIFO)。这就是我们在 BFS 中使用队列的原因。
2.广度优先搜索-模板
之前,我们已经介绍了使用 BFS 的两个主要方案:遍历
或找出最短路径
。通常,这发生在树或图中。正如我们在章节描述中提到的,BFS 也可以用于更抽象的场景中。
在特定问题中执行 BFS 之前确定结点和边缘非常重要。通常,结点将是实际结点或是状态,而边缘将是实际边缘或可能的转换。
模板1(伪代码)
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
add next to queue;
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
- 如代码所示,在每一轮中,队列中的结点是等待处理的结点。
- 在每个更外一层的 while 循环之后,我们距离根结点更远一步。变量 step 指示从根结点到我们正在访问的当前结点的距离。
模板2(伪代码)
有时,确保我们永远不会访问一个结点两次
很重要。否则,我们可能陷入无限循环。如果是这样,我们可以在上面的代码中添加一个哈希集来解决这个问题。这是修改后的伪代码:
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
Set<Node> used; // store all the used nodes
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
add root to used;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
if (next is not in used) {
add next to queue;
add next to used;
}
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
有两种情况你不需要使用哈希集:
- 你完全确定没有循环,例如,在树遍历中;
- 你确实希望多次将结点添加到队列中。
三、栈:后入先出的数据结构
1.后入先出的数据结构(LIFO)
在 LIFO 数据结构中,将首先处理添加到队列中的最新元素。
与队列不同,栈是一个 LIFO 数据结构。通常,插入操作在栈中被称作入栈 push 。与队列类似,总是在堆栈的末尾添加一个新元素。但是,删除操作,退栈 pop ,将始终删除队列中相对于它的最后一个元素。
2.栈-用法
内置的栈库:
#include <iostream>
int main() {
// 1. Initialize a stack.
stack<int> s;
// 2. Push new element.
s.push(5);
s.push(13);
s.push(8);
s.push(6);
// 3. Check if stack is empty.
if (s.empty()) {
cout << "Stack is empty!" << endl;
return 0;
}
// 4. Pop an element.
s.pop();
// 5. Get the top element.
cout << "The top element is: " << s.top() << endl;
// 6. Get the size of the stack.
cout << "The size is: " << s.size() << endl;
}
实践:最小栈
#define MAXNUM 30000
typedef int Datetype;
typedef struct {
int top;
int min;
Datetype data[MAXNUM];
} MinStack;
MinStack* minStackCreate() {
MinStack *p=(MinStack*)malloc(sizeof(MinStack));
p->min=0;
p->top=0;
return p;
}
void minStackPush(MinStack* obj, int val) {
if(obj){
if(obj->top==0){
obj->data[obj->top]=val;
obj->min=val;
obj->top++;
}else{
if(val<obj->min){
obj->data[obj->top]=val;
obj->min=val;
obj->top++;
}else{
obj->data[obj->top]=val;
obj->top++;
}
}
}
}
void minStackPop(MinStack* obj) {
if(obj&&obj->top!=0){
if(obj->data[obj->top-1]>obj->min){
obj->top--;
}else{
obj->top--;
if(obj->top!=0){
obj->min=obj->data[obj->top-1];
int i;
for(i=0;i<obj->top;i++){
if(obj->data[i]<obj->min){
obj->min=obj->data[i];
}
}
}
}
}
}
int minStackTop(MinStack* obj) {
return obj->data[obj->top-1];
}
int minStackGetMin(MinStack* obj) {
return obj->min;
}
void minStackFree(MinStack* obj) {
obj->top=0;
}
实践:有效的括号
class Solution {
public:
bool isValid(string s) {
stack<char> bracket;
int i;
for(i=0;i<s.length();i++){
switch(s[i]){
case '(':
bracket.push(')');
break;
case '[':
bracket.push(']');
break;
case '{':
bracket.push('}');
break;
case ')':
if(!bracket.empty()&&bracket.top()==')'){
bracket.pop();
}else{
return false;
}
break;
case ']':
if(!bracket.empty()&&bracket.top()==']'){
bracket.pop();
}else{
return false;
}
break;
case '}':
if(!bracket.empty()&&bracket.top()=='}'){
bracket.pop();
}else{
return false;
}
break;
}
}
if(bracket.empty()){
return true;
}else{
return false;
}
}
};
实践:每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例 2:
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> ret(temperatures.size());
int i;
stack<int> b;
for(i=0;i<temperatures.size();i++){
if(b.empty()){
b.push(i);
}else{
if(temperatures[i]>temperatures[b.top()]){
while(!b.empty()&&temperatures[i]>temperatures[b.top()]){
ret[b.top()]=i-b.top();
b.pop();
}
b.push(i);
}else{
b.push(i);
}
}
}
while(!b.empty()){
ret[b.top()]=0;
b.pop();
}
return ret;
}
};
实践:逆波兰表达式求值
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long int> num;
int i;
for(i=0;i<tokens.size();i++){
if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/"){
long long int a=num.top();
num.pop();
long long int b=num.top();
num.pop();
int c;
if(tokens[i]=="+"){
c=a+b;
}else if(tokens[i]=="-"){
c=b-a;
}else if(tokens[i]=="*"){
c=a*b;
}else{
c=b/a;
}
num.push(c);
}else{
int n=stoi(tokens[i]);
num.push(n);
}
}
return num.top();
}
};
四、*栈和深度优先搜索
先决条件:树的遍历树的遍历
与 BFS 类似,深度优先搜索(DFS)是用于 在树/图中遍历/搜索 的另一种重要算法。也可以在更抽象的场景中使用。
正如树的遍历中所提到的,我们可以用 DFS 进行 前序遍历,中序遍历 和 后序遍历。在这三个遍历顺序中有一个共同的特性:除非我们到达最深的结点,否则我们永远不会回溯 。
这也是 DFS 和 BFS 之间最大的区别,BFS永远不会深入探索,除非它已经在当前层级访问了所有结点。
通常,我们使用递归实现 DFS。栈在递归中起着重要的作用。在本章中,我们将解释在执行递归时栈的作用。我们还将向你展示递归的缺点,并提供另一个 **没有递归 **的 DFS 实现。
1.栈和DFS
与 BFS 类似,深度优先搜索
(DFS)也可用于查找从根结点到目标结点的路径。
在 DFS 中找到的第一条路径并不总是最短的路径。
2.DFS-模板1
3.DFS-模板2
(模板都是java的…)
作者:力扣 (LeetCode)
链接:https://leetcode.cn/leetbook/read/queue-stack/k6zxm/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。