栈结构解析及其应用

导言#

随着生活水平的不断提高,越来越多的轿车走进千家万户,不过这也带来了一个严重的问题——停车位的寻找变得困难,因此在生活中我们经常会遇到把车停在不应该停的位置,导致半夜接到电话要求挪车或者收了罚单。现在我们来想象一个情景,我要在一个只有一个出口的窄巷子停车,那么停在内部的车想要开出来,就必须等在最外面的车开走,新的车停进来,只能停在窄巷子的最外面,最里面的车想要开出来就必须让其他所有的车都开走。

这真是一种我们很不愿意见到的情景,好在现实中司机一般不会做这种事情。如果我们把这个窄巷子抽象成一个线性表,车当做表中的元素,我们会发现这个线性表只能对表尾操作,放入新的元素就必须从表尾放入,由于尾部的元素把表的唯一出口堵死了,因此想要把表中的元素拿出,就只能拿出表尾,即最后一个元素。那么这种特殊的顺序表就是一种新的数据结构——栈,它的特点是先进后出,后进先出。栈在计算机相关领域中使用广泛,举个大家熟悉的例子,例如浏览器的后退功能,同个这个按键,我们可以查看单个网页的页面之前查看过的连接,而且这个按键的操作也是单向的,后查看的链接会被先查看。

什么是栈?#

栈(stack)又名堆栈,它是一种运算受限的线性表,受限于该结构仅能在表尾进行元素的插入和删除操作。首先栈本质上还是一个线性表,只是有一些操作上较为特殊,栈中的元素具有仍然具有线性关系。在允许进行插入和删除的一段被称之为栈顶,表的另一端被称为栈底,若在栈中没有任何元素,栈就被称为空栈,栈结构的插入操作被称为压栈,删除操作被称为退栈出栈。栈最鲜明的特点就是先进后出,后进先出,出栈的元素一定是位于栈顶的元素,在栈顶的元素出栈之后,下一个元素就成为新的栈顶,当栈底的元素执行出栈操作之后栈就成为了空栈。

栈的抽象数据类型#

Copy Highlighter-hljs
ADT Stack { Data: D = {ai | 1 ≤ i ≤ n, n ≥ 0, ai 为 ElemType 类型} //同线性表 Relation: R = { <ai,ai+1> | ai,ai+1 ∈ D, i = 1, i ∈ (0,n)} //同线性表 Operation: InitStack(&s); //初始化栈,开辟一个空间给栈 s StackEmpty(*s); //判断栈是否为空栈,若为空栈返回 true,否则返回 false Push(&s,e); //进栈操作,将元素 e 加入栈结构中并使其作为栈顶 Pop(&s,&e); //出栈操作,将位于栈顶的元素删除,并赋值给变量 e GetTop(s,&e); //取栈顶操作,若栈不为空栈,返回栈顶元素并赋值给变量 e ClearStack(&s); //清空栈,将栈中的所有元素清空,即将栈变为空栈 DestroyStack(&s); //销毁栈,将释放栈的空间 }

顺序栈及其基本操作#

顺序栈#

栈是一种特殊的线性表,也自然可以使用顺序存储结构来实现。在 C\C++ 中,我们对于顺序存储往往使用数组来描述,因此我们需要为一个数组选择栈底和栈顶,为了方便描述空栈判定和栈满判定,我们使用下标为 0 的位置作为栈底,当栈顶的下标为数组元素上限时即为栈满,为了时刻定位栈顶的位置,需要定义一个栈顶指针作为游标来辅助。

顺序栈的结构体定义

Copy Highlighter-hljs
#define MAXSIZE 100 typedef struct { ElemType data[MAXSIZE]; int top; //栈顶指针 }SqStack;

初始化栈#

为一个新建立的栈 s 分配足够的空间,由于空栈没有任何元素,因此栈顶指针将初始化为 -1。

Copy Highlighter-hljs
void InitStack(SqStack s) { s = new SqStack; s->top = -1; //栈顶指针将初始化为 -1,表示没有任何元素 }

空栈判断#

某个结构为空一直是一个显然而敏感的状态,如果不妥善处理就会出现严重的异常,就例如对空栈执行出栈操作,就会出现非法读取的情况。因此虽然空栈判断的代码简单,但是值得我们重视。函数在栈为空栈时返回 true,反之返回 false。

Copy Highlighter-hljs
bool StackEmpty(SqStack *s) { if(s->top == -1) { return true; } return false; }

进栈操作#

由于栈是一种操作受限的线性表,因此进栈操作是其核心操作之一。进栈的关键在于只能在表尾进行插入,并且当栈的空间为满的时候,不能入栈。函数将在栈不为满栈的情况下,在栈顶指针 top 处插入元素 e 并使其自增 1,插入成功返回 true,否则返回 false。时间复杂度 O(1)。

Copy Highlighter-hljs
bool Push(SqStack &s,ElemType e) { if(s->top == MAXSIZE - 1) //判断是否栈满 { return false; } s->data[s->top++] = e; //入栈 return true; }

出栈操作#

同进栈,出栈也是很重要的操作,出栈的关键在于只能在表尾进行插入,并且当栈的空间为空的时候,不能出栈。函数将在栈不为空栈的情况下,将位于栈顶指针 top 处的元素出栈并赋值给变量 e ,top 需要并使其自减 1,退栈成功返回 true,否则返回 false。时间复杂度 O(1)。

Copy Highlighter-hljs
bool Pop(SqStack &s,ElemType e) { if(StackEmpty(s)) //判断是否为空栈 { return false; } e = s->data[s->top--]; //退栈 return true; }

取栈顶操作#

取栈顶操作与出栈操作不同的是,取栈顶操作只需把栈顶元素赋值给变量 e,无需对栈进行修改。时间复杂度 O(1)。

Copy Highlighter-hljs
bool GetTop(SqStack &s,ElemType e) { if(StackEmpty(s)) //判断是否为空栈 { return false; } e = s->data[s->top]; //取栈顶 return true; }

链栈及其基本操作#

链栈#

当栈使用链式存储结构来存储时,可以建立单链表来描述,显然以链表的表头结点作为栈顶是最方便的。使用连式存储结构的优点在于,栈的空间在一般情况下不需要考虑上限。对于链栈来说,我们可以不设置头结点。

链栈的结构体定义#

Copy Highlighter-hljs
typedef struct StackNode { ElemType data; struct StackNode *next; }Node,*Stack;

初始化栈#

初始化的操作是为了构造一个空栈,在不设置头结点的情况下,我们把栈顶指针搞成 NULL 即可。

Copy Highlighter-hljs
bool InitStack(Stack &s) { s = NULL; return true; }

空栈判断#

某个结构为空一直是一个显然而敏感的状态,如果不妥善处理就会出现严重的异常,就例如对空栈执行出栈操作,就会出现非法读取的情况。因此虽然空栈判断的代码简单,对于链栈值得我们重视。函数在栈为空栈时返回 true,反之返回 false。

Copy Highlighter-hljs
bool StackEmpty(Stack *s) { if(s == NULL) { return true; } return false; }

进栈操作#

对于链栈的进栈操作,我们不需要判断是否出现栈满的情况,只需要用头插法引入新结点即可,插入成功返回 true,否则返回 false。时间复杂度 O(1)。

Copy Highlighter-hljs
bool Push(Stack &s,ElemType e) { Stack ptr = new Node; //为新结点申请空间 ptr->next = s; //修改新结点的后继为 s 结点,入栈 ptr->data = e; s = ptr; //修改栈顶为 ptr return true; }

出栈操作#

同顺序栈,当栈的空间为空的时候,不能出栈,函数将在栈不为空栈的情况下,需要把栈顶结点的空间释放掉,退栈成功返回 true,否则返回 false。时间复杂度 O(1)。

Copy Highlighter-hljs
bool Pop(Stack &s,ElemType e) { Stack ptr; if(StackEmpty(s)) //判断是否为空栈 { return false; } e = s->data; //将栈顶元素赋值给 e ptr = S; //拷贝栈顶元素 S = S->next; //退栈 delete ptr; //释放原栈顶元素结点的空间 return true; }

取栈顶操作#

当栈非空时,把栈顶元素赋值给变量 e,时间复杂度 O(1)。

Copy Highlighter-hljs
bool GetTop(SqStack &s,ElemType e) { if(StackEmpty(s)) //判断是否为空栈 { return false; } e = s->data; //取栈顶 return true; }

双端栈#

实现目标#

复杂的操作由基本操作组合而成#

我们这么去理解,假设我们已经定义了两个栈,开辟了一定的空间,那么会不会出现一个栈满了,而另一个栈还有很多空间呢?那么我们在这个时候就很希望能够让第一个栈使用第二个栈的空间,从理论上讲,这样是完全可行的,因为我们只需要让这两个栈能够分别找到自己的栈顶和栈底即可。例如在一个数组中,我们可以让数组的始端和末端分别为两个栈的栈底,再通过操作游标来实现对栈顶的描述。对于栈满的判断呢?只要两个栈的栈顶不见面,栈就不为满栈。


代码实现#

建立双端栈#

Copy Highlighter-hljs
Stack CreateStack(int MaxSize) //建立双端栈 { Stack sak = (Stack)malloc(sizeof(struct SNode)); sak->MaxSize = MaxSize; sak->Data = (ElementType*)malloc(MaxSize * sizeof(ElementType)); sak->Top1 = -1; sak->Top2 = MaxSize; return sak; }

入栈操作#

Copy Highlighter-hljs
bool Push(Stack S, ElementType X, int Tag) //入栈 { if (S->Top2 - 1 == S->Top1) { printf("Stack Full\n"); return false; } if (Tag == 1) { S->Data[++S->Top1] = X; } else { S->Data[--S->Top2] = X; } return true; }

出栈操作#

Copy Highlighter-hljs
ElementType Pop(Stack S, int Tag) //出栈 { if (Tag == 1) { if (S->Top1 < 0) { printf("Stack %d Empty\n",Tag); return ERROR; } else { return S->Data[S->Top1--]; } } else { if (S->Top2 == S->MaxSize) { printf("Stack %d Empty\n",Tag); return ERROR; } else { return S->Data[S->Top2++]; } } }

栈的应用-符号配对#

应用情景#

情景分析#

由于我们只关注表达式的括号是否是成双成对的,因此只需要获取我们所需即可。当我获取第一个括号时,虽然后面可能会有贼多括号,但是我们只继续接受下一个括号,若下一个括号仍然为左括号,那么这个括号需要配对的优先级是高于第一个左括号的。继续读取,若下一个括号为右括号,就拿来和配对优先级较高的第二个括号比对,若成功配对则消解第二个括号,而第一个括号需要配对的优先级就提升了。经过分析我们发现,使用栈结构来描述这个过程极为合适。

伪代码#

代码实现#

Copy Highlighter-hljs
#include <iostream> #include <stack> #include <string> using namespace std; int main() { string equation; stack<char> brackets; //存储被配对的左括号 int flag = 0; cin >> equation; for (int i = 0; equation[i] != 0; i++) { if (equation[i] == '(' || equation[i] == '[' || equation[i] == '{') //第 i 个字符是左括号 { brackets.push(equation[i]); } else if (brackets.empty() && (equation[i] == ')' || equation[i] == ']' || equation[i] == '}')) { flag = 1; //第 i 个字符是右括号但栈是空栈 break; } else if (equation[i] == ')' && brackets.top() == '(') //栈顶括号与右括号配对 { brackets.pop(); } else if (equation[i] == ']' && brackets.top() == '[') { brackets.pop(); } else if (equation[i] == '}' && brackets.top() == '{') { brackets.pop(); } } if (flag == 1) //输出配对结果 { cout << "no"; } else if (brackets.empty() == true) { cout << "yes"; } else { cout << brackets.top() << "\n" << "no"; } return 0; }

栈的应用-逆波兰式的转换#

逆波兰式#

众所周知,对于一个算式而言,不同的运算符有优先级之分,例如“先乘除,后加减”,如果是我们人工进行计算的话,可以用肉眼观察出算式的运算顺序进行计算。可是对于计算机而言,如果是一个一个读取算式进行计算的话,可能不能算出我们想要的答案,因为这么做是没有优先级可言的。想要让计算机实现考虑优先级的算式计算,我们首先要先找到一种算式的描述方式,这种方式不需要考虑运算符优先级。
逆波兰式(Reverse Polish notation,RPN,或逆波兰记法),也叫后缀表达式(将运算符写在操作数之后的表达式),是波兰逻辑学家卢卡西维奇提出的,例如“2 + 3 * (7 - 4) + 8 / 4”这样一个表达式,它对应的后缀表达式是“2 3 7 4 - * + 8 4 / +”,这种表达式的计算方法是遇到运算符就拿前面的两个数字来计算,用这个数字替换掉计算的两个数字和运算符,直到得出答案。

应用情景#

伪代码#

代码实现#

Copy Highlighter-hljs
#include <iostream> #include <stack> #include <queue> #include <string> #include <map> using namespace std; int main() { string str; stack<char> sign; //存储符号 queue<char> line; //存储转换好的逆波兰式,便于后续实现计算 map<char, int> priority; priority['('] = 3; //为符号设置优先级 priority[')'] = 3; priority['*'] = 2; priority['/'] = 2; priority['+'] = 1; priority['-'] = 1; int flag = 0; cin >> str; for (int i = 0; i < str.size(); i++) { //读取到数字 if (((i == 0 || str[i - 1] == '(') && (str[i] == '+' || str[i] == '-')) || (str[i] >= '0' && str[i] <= '9')) { line.push('#'); if (str[i] != '+') { line.push(str[i]); } while ((str[i + 1] >= '0' && str[i + 1] <= '9') || str[i + 1] == '.') { line.push(str[++i]); } } else //读取到运算符 { if (str[i] == ')') //运算符是右括号 { while (!sign.empty() && sign.top() != '(') //左括号之后的运算符全部出栈 { line.push('#'); line.push(sign.top()); sign.pop(); } sign.pop(); continue; } else { while (!sign.empty() && sign.top() != '(' && priority[str[i]] <= priority[sign.top()]) { line.push('#'); line.push(sign.top()); sign.pop(); } } sign.push(str[i]); } } while (!sign.empty()) //将栈内剩余的符号出栈 { line.push('#'); line.push(sign.top()); sign.pop(); } while (!line.empty()) { if (flag == 0 && line.front() == '#') { flag++; } else if(line.front() == '#') { cout << ' '; } else { cout << line.front(); } line.pop(); } return 0; }

迷宫寻路(深度优先)#

左转博客——栈和队列应用:迷宫问题

八皇后问题(栈实现)#

左转我另一篇博客八皇后问题——回溯法思想运用

参考资料#

《大话数据结构》—— 程杰 著,清华大学出版社
《数据结构教程》—— 李春葆 主编,清华大学出版社
《数据结构与算法》—— 王曙燕 主编,人民邮电出版社
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社

posted @   乌漆WhiteMoon  阅读(1589)  评论(7编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
CONTENTS