前言
- 🌰欢迎大家来到OpenAll_Zzz的博客,在这里我们一起努力,共同进步!
- 🎁这一期分享经典的数据结构——栈和队列,以及与栈和队列有关的经典面试OJ题。
- 🎄本文内容为OpenAll_Zzz原创,转载的小伙伴还请标注一下来源。
- 🎁欢迎大家评论📖、转发📤、关注👓,如果文章对你有帮助的话,希望能得到你的一个大大的赞👍。
一、栈
理解栈
- 栈的概念
-
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。 ——维基百科
- 一说到栈,那第一反应就是先进后出(FILO, Frist In Last Out)。
- 这里的先进后出指的是:先进入栈(压栈、入栈、进栈)的元素我们只能之后来访问它(出栈、退栈),所以之后进来的元素我们会先访问到,也就有了后进先出这样的说法,这两种说法是等价的。注意,不可以强制改变栈内基本操作的对象,例如我们用
stack[0]
来访问顺序栈的栈底元素,虽然可以得到我们希望的结果,但是这样是不合规矩的,进一步讲我们不可以改变常用的数据结构的内部结构来达到目的,这样就丧失了设计数据结构的初衷。回到问题本身,我们需要先将每个元素出栈,直到栈中只有一个元素时我们才得到了栈底元素。整个过程只使用了出栈函数,是符合要求的;另外整个过程看起来像是做了很多无用功,其实正是这些无用功才保证了我们在设计软件时对数据结构设计时的正确性做保障,与整个工程相比这是不足为惜的。 -
在这里举个例子帮助理解先进后出。小明有个习惯就是喜欢整理自己的书本,所以从小学到高中毕业,小明每一年学期末将所的课本都放到书箱中(压栈),有一天小明回忆起小学的欢乐时光 ,于是他找起了他小学时的课本,这个时候他就得把初中和高中的书本先移出书箱(出栈),再去拿起小学的课本来回忆童年…(此处省略无数个美好时刻🎉)。
- 上方例子中的小明需要先把初中和高中的课本“先访问”,也就是后来放进来的书会先访问到,即后进先出;后来再访问到小学的课本,也就是最先放进来的会后访问到,即先进后出。
二、队列
理解队列
- 队列的概念
-
队列(queue)是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。 ——维基百科
- 同样的,提到队列,想到的就是先进先出(FIFO, Frist In Frist Out)。
- 这里的先进先出指的是:先进入队列的元素我们会优先访问到,后进入的元素需要等先进来的元素被访问后才会被进行访问,且这种顺序关系不可以强制改变,这样的请况下才是一个先进先出的队列。
-
在我们日常生活中,排队的队列就是一种队列(暂且不考虑有插队、提前离队等等情况),整个队伍最前方的完成相应的工作后就可以从最前方离队了,即
出队
,有需要办理业务的人员必须从队伍的最后接上,即入队
。 - 上方例子中很形象的帮助我们理解了队列的先进先出的结构。
- 下面我们讲解三道有关栈和队列的经典面试题。
三、栈和队列的经典面试题
LeetCode 232. 用栈实现队列
原题链接:LeetCode 232.用栈实现队列
关键点:
要实现队列的先进先出,我们只能使用两个栈以及其基本的操作
push()、pop()、top()、empty()、size()
,我们先利用一个栈来存放预先进入栈的数据,我们称其为输出栈,用另外一个栈来进行出栈和获取队头元素,我们称其为输出栈,来达到实现队列的目的。具体是当我们要获取当前队列(两个栈所表示的)
的队头元素时,我们将输入栈导入输出栈,那么此时输出栈的栈顶元素刚好就是队列的队头元素。我们通过两个栈将原始访问的后进先出(栈)
顺序改为先进先出(队列)
的顺序。
C++代码
class MyQueue {
public:
stack<int> st1; // 输入栈
stack<int> st2; // 输出栈
MyQueue() {
}
void push(int x) {
st1.push(x);
}
int pop() {
if(st2.empty())
{
while(!st1.empty()) // 将输入栈导入输出栈
{
st2.push(st1.top());
st1.pop();
}
}
int ans = st2.top();
st2.pop();
return ans;
}
int peek() {
if(st2.empty())
{
while(!st1.empty()) // 当输出栈为空的时候需要从输入栈中导入数据
{
st2.push(st1.top());
st1.pop();
}
}
return st2.top();
}
bool empty() {
if(st1.empty() && st2.empty()) return true;
return false;
}
};
C代码
// 注:没有将栈被封装起来,所以代码看的比较臃肿,不过思想是都是一致的
typedef struct {
int* st1; // 输入栈
int* st2; // 输出栈
int top1;
int top2;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* res = (MyQueue*)malloc(sizeof(MyQueue));
res->st1 = (int*)malloc(sizeof(int) * 100);
res->st2 = (int*)malloc(sizeof(int) * 100);
res->top1 = 0;
res->top2 = 0;
return res;
}
void myQueuePush(MyQueue* obj, int x) {
obj->st1[obj->top1++] = x;
}
int myQueuePop(MyQueue* obj) {
if(obj->top2 == 0)
{
while(obj->top1 != 0) // 将输入栈导入输出栈,改变其顺序,先进后出再进行一次先进后出就改变成了先进先出
{
obj->st2[obj->top2++] = obj->st1[--(obj->top1)];
}
}
return obj->st2[--(obj->top2)];
}
int myQueuePeek(MyQueue* obj) {
if(obj->top2 == 0)
{
while(obj->top1 != 0)
{
obj->st2[obj->top2++] = obj->st1[--(obj->top1)];
}
}
return obj->st2[obj->top2 - 1];
}
bool myQueueEmpty(MyQueue* obj) {
return (obj->top1 == 0 && obj->top2 == 0) ? true : false;
}
void myQueueFree(MyQueue* obj) {
free(obj->st1);
free(obj->st2);
}
LeetCode 225. 用队列实现栈
原题链接:LeetCode 225. 用队列实现栈
关键点:
数据被
push()
进队列时,我们称其为输入队列,这时只有输入队列的队头才可以进行pop
操作,但是在输入队列中的队尾元素才是我们需要访问的栈顶(两个队列实现的栈)
元素,所以我们借助另外一个队列,称其为中间队列(暂时存放数据)
,我们将输入队列的元素导入中间队列,直到输入队列中只剩一个元素时,我们将其pop()
,就实现了栈的出栈操作。此时我们为了操作的统一性,我们只将中间队列当成暂时存放元素的容器,将输入队列中元素导入(只使用栈的pop()和push()
操作)中间队列后,在对输入队列进行pop()、top()
之后(如果是top()操作我们还得将输入队列的最后一个元素导入到中间队列)
,我们最后将中间队列存放的元素导入到输入队列中。之后i的每次访问栈顶元素时我们都进行相同的操作,这样我们使用两个队列就实现了栈的先进后出。
C代码
// 先创建队列
typedef struct
{
int* queue;
int head;
int tail;
}myQueue;
myQueue* init()
{
myQueue* res = (myQueue*)malloc(sizeof(myQueue));
res->queue = (int*)malloc(sizeof(int) * 100);
res->head = res->tail = 0;
return res;
}
void push(myQueue* obj, int x)
{
obj->queue[obj->tail++] = x;
}
int peek(myQueue* obj)
{
return obj->queue[obj->head];
}
int pop(myQueue* obj)
{
return obj->queue[obj->head++];
}
bool empty(myQueue* obj)
{
return obj->head == obj->tail ? true : false;
}
int size(myQueue* obj)
{
return obj->tail - obj->head;
}
// 以上是实现栈所需的队列结构
typedef struct {
myQueue* q1; // 输入队列
myQueue* q2; // 中间队列
} MyStack;
MyStack* myStackCreate()
{
MyStack* res = (MyStack*)malloc(sizeof(MyStack));
res->q1 = init();
res->q2 = init();
return res;
}
void myStackPush(MyStack* obj, int x)
{
push(obj->q1, x);
}
int myStackPop(MyStack* obj) {
while(size(obj->q1) != 1 && size(obj->q1) != 0)
{
push(obj->q2, pop(obj->q1)); // 将输入队列导入中间队列
}
int tmp = pop(obj->q1);
while(!empty(obj->q2))
{
push(obj->q1, pop(obj->q2)); // 将中间队列存放的数据导回输入队列
}
return tmp;
}
int myStackTop(MyStack* obj)
{
int top = 0;
while(size(obj->q1) != 1 && size(obj->q1) != 0)
{
push(obj->q2, pop(obj->q1)); // 将输入队列导入中间队列
}
if(size(obj->q1) == 1) top = peek(obj->q1);
push(obj->q2, pop(obj->q1)); // top()时还需将队尾(两个队列实现的栈的栈顶)元素导入中间队列
while(!empty(obj->q2))
{
push(obj->q1, pop(obj->q2)); // 将中间队列存放的数据导回输入队列
}
return top;
}
bool myStackEmpty(MyStack* obj)
{
if(empty(obj->q1) && empty(obj->q2)) return true;
else return false;
}
void myStackFree(MyStack* obj)
{
free(obj->q1->queue);
free(obj->q1);
free(obj->q2->queue);
free(obj->q2);
}
LeetCode 20. 有效的括号
原题链接:LeetCode 20. 有效的括号
关键点:
本题我们需要判断所给出的字符串中出现的
'('、'['、'{'三种左括号
是否匹配其相应的右括号。关键的一点是,出现的右括号与其相邻的左元素必须匹配,例如出现了']',那么其左边的的元素必须是'['才可以匹配上
,如果其左边没有元素或者左边有元素但是不匹配,我们就判定出了该字符串中的括号不匹配;如果是相匹配的,那我们需要找下一个出现的右括号,这个时候我们就需要使用栈来保存我们所存放的左括号集,因为我们要对每一个出现的右括号来判断是否有其合法的左括号与其匹配上。
-
注:
"([{}])"
中'}'
的相邻左元素就是已经入栈的'{'
。在字符串中能直接反映出来。 -
细节:我们遇到左括号就将其压入栈,因为栈顶元素是我们遍历的下一个元素的相邻的左元素,所以遇到右括号我们将栈顶元素出栈,也就是将右括号的相邻左元素与该右括号进行比对判断是否匹配。特殊的,如果遇到右括号,这个时侯我们出栈时,发现栈已经为空,也就是没有左括号与其比对,我们能直接判断出该不匹配。
C++代码
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(int i = 0; i < s.size(); i++){
if(s[i] == '(' || s[i] == '{' || s[i] == '[') st.push(s[i]);
else{
if(st.empty()) return false;
if(s[i] == ')' && st.top() != '(') return false;
if(s[i] == ']' && st.top() != '[') return false;
if(s[i] == '}' && st.top() != '{') return false;
st.pop();
}
}
return st.empty();
}
};