前置知识:栈和队列的基本概念
栈在括号匹配中的应用
基本算法思想:读取一个字符串,依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。如果需要增强算法健壮性,还可以加上判断读入的字符是否为括号。
匹配失败的情况:①左右括号不匹配;②右括号无匹配;③左括号多余。
上代码:(输入一个全是左右括号的字符串,若全都能匹配则输出OK,若匹配失败输出ERROR)
#define _CRT_SECURE_NO_WARNINGS 1
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
#define MaxSize 100
typedef struct{
char data[MaxSize];//静态数组实现顺序栈
int top;//栈顶指针
}SqStack;
void InitStack(SqStack& S) {//初始化栈
S.top = -1;//栈顶指针指向-1
return;
}
bool StackEmpty(SqStack& S) {//判断栈是否为空,看栈顶指针是否指向-1
return S.top == -1 ? true : false;
}
bool Push(SqStack& S, char ch) {//入栈
if (S.top == MaxSize - 1)return false;//若栈满则报错
S.data[++S.top] = ch;//栈顶指针先+1,再让新元素入栈
return true;
}
bool Pop(SqStack& S, char& ch) {//出栈,并用ch返回栈顶元素
if (StackEmpty(S))return false;//栈空报错
ch = S.data[S.top--];//先记录栈顶元素,再让栈顶指针-1
return true;
}
bool bracketCheck(char str[], int len) {//检查字符串
SqStack S;//声明一个栈
InitStack(S);//初始化栈S
char ch1,ch2;//临时变量
for (int i = 0; i < len; ++i) {
ch1 = str[i];//按位读取字符串
if (ch1 == '(' || ch1 == '[' || ch1 == '{') {//若为左括号则入栈
if (!Push(S, ch1))return false;//入栈失败报错
continue;//入栈成功则继续处理字符串的下一位
}
else if (!Pop(S, ch2))return false;//否则出栈,且判断出栈是否成功
if (ch1 == ')' && ch2 != '(')return false;//小括号匹配
if (ch1 == ']' && ch2 != '[')return false;//中括号匹配
if (ch1 == '}' && ch2 != '{')return false;//大括号匹配
}
return StackEmpty(S);//最后判断栈是否为空,若为空则返回true,否则false
}
int main() {
char s[MaxSize];
scanf("%s", s);//读入字符串
if(bracketCheck(s,strlen(s)))printf("OK\n");//若匹配通过输出OK
else printf("ERROR\n");//否则输出ERROR
return 0;
}
输入样例1:
{[[()]()]}
输出样例1:
OK
输入样例2:
{{[[]}]()}
输出样例2:
ERROR
完整代码也可看我的Github:传送门
栈在表达式求值中的应用
中缀表达式(如3 + 4)是人们常用的算术表达式,操作符以中缀形式处于操作式的中间。与前缀表达式和后缀表达式不同的是,中缀表达式中必须使用括号来指示运算次序,而前缀表达式和后缀表达式在书写时就已考虑了运算符的优先级,无需括号。
前缀表达式,又称波兰式(Polish Notation),是一种运算符在操作数前的算术表达式(如+ 3 4)。
后缀表达式,又称逆波兰式(Reverse Polish Notation),是一种运算符在操作数后的算术表达式(如3 4 +)。
中缀转后缀的手算方法:
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「左操作数 右操作数 运算符」的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
例如:A+B*(C-D)-E/F
转化结果是:ABCD-*+EF/-
转化过程中需遵从“左优先”原则:即只要左边的运算符能够计算,就优先计算左边的。
(同理,中缀转前缀遵从“右优先”原则)
中缀转前缀的手算方法:
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「运算符 左操作数 右操作数」的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
例如:A+B*(C-D)-E/F
转化结果:+A-*B-CD/EF
手算例题:中缀表达式为
((15/(7-(1+1)))*3)-(2+(1+1))
则前缀表达式为:- * / 15 - 7 + 1 1 3 + 2 + 1 1
后缀表达式为:15 7 1 1 + - / 3 * 2 1 1 + + -
用栈实现后缀表达式的计算:
①从左往右扫描下一个元素,直到处理完所有元素;
②若扫描到操作数则压入栈,并回到①,否则执行③;
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①。
代码实现如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
#define MaxSize 100
int num[MaxSize] = { 0 }, top = -1;
//静态数组存放数据栈,初始化栈顶指针为-1
bool is_num(char ch) {//判断字符是否为数字字符
return ('0' <= ch && ch <= '9') ? true : false;
}
int main() {
int len = 0;
char str[MaxSize],ch;//str字符数组记录后缀表达式
while (ch = getchar()) {//按位读入
if (ch == '\n')break;//读到换行符则结束输入
str[len++] = ch;//否则将读到的字符存入字符数组
}
int a;//临时数字
for (int i = 0; i < len; ++i) {//按位扫描后缀表达式
if (str[i] == ' ')continue;//若为空格则跳过
if (is_num(str[i])) {//若当前位为数字
a = 0;
for (; i < len; ++i) {//循环向后扫描,直到当前位不为数字为止
if (is_num(str[i]))a = a * 10 + (int)(str[i] - '0');//如果是数字则循环累加进行记录
else break;//不是数字则跳出循环
}
i--;//复位
num[++top] = a;//将操作数压入栈中
continue;
}
else {//若当前位不是数字
if (str[i] == '+') {//若为+号
a = num[top--];//弹出栈顶元素,用a记录
num[top] += a;//将新的栈顶元素数值加上a,完成加法操作
}
else if (str[i] == '-') {//若为-号
a = num[top--];//弹出栈顶元素,用a记录
num[top] -= a;//将新的栈顶元素数值减去a,完成减法操作
}
else if (str[i] == '*') {//乘除法与加减法逻辑类似
a = num[top--];
num[top] *= a;
}
else if (str[i] == '/') {
a = num[top--];
num[top] /= a;
}
}
}
printf("%d\n", num[top]);//最后栈中剩余的元素即是后缀表达式的值
return 0;
}
输入样例:
15 7 1 1 + - / 3 * 2 1 1 + + -
输出样例:
5
完整代码也可看我的Github:传送门
需要注意的是,我写的这段代码只是最简单直观的计算后缀表达式值的方法,没有对非法表达式的判断。
用栈实现前缀表达式的计算:
①从右往左扫描下一个元素,直到处理完所有元素;
②若扫描到操作数则压入栈并回到①,否则执行③;
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①。
由于408考研初试中,对前缀表达式考察频率不如后缀表达式高,所以我偷懒没写代码。(逃
用栈实现中缀表达式的计算:
初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)。
后缀转中缀以及前缀转中缀的算法本质上就是逆着来就行,故不作赘述。
会手推就行。
中缀表达式转后缀表达式(机算):
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾。可能遇到三种情况:
①遇到操作数——直接加入后缀表达式。
②遇到界限符——遇到“(” 直接入栈,遇到“)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出“(” 为止。注意:“(” 不加入后缀表达式。
③遇到运算符——依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
代码实现如下,调试花了我一个多小时 (大悲
#define _CRT_SECURE_NO_WARNINGS 1
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
#define MaxSize 100
typedef struct LNode {
char data;
struct LNode* next;
}LNode,*SqStack;//单链表方式定义顺序栈
void InitStack(SqStack& S) {//初始化栈
S = (LNode*)malloc(sizeof(LNode));
if (S == NULL)return;
S->next = NULL;
return;
}
bool is_Empty(SqStack& S) {//判断栈是否为空
return S->next == NULL ? true : false;
}
bool Push(SqStack& S, char ch) {//新元素入栈
LNode* p = (LNode*)malloc(sizeof(LNode));
if (p == NULL)return false;
p->data = ch;
p->next = S->next;
S->next = p;
return true;
}
bool Pop(SqStack& S, char& ch) {//栈顶元素出栈,并用ch返回其值
if (is_Empty(S))return false;
LNode* p = S->next;
ch = p->data;
S->next = p->next;
free(p);
return true;
}
bool Query(SqStack& S, char& ch) {//仅查询栈顶元素,用ch返回
if (is_Empty(S))return false;
ch = S->next->data;
return true;
}
bool is_Op(char ch) {//判断ch是否为操作符
if (ch == '+')return true;
if (ch == '-')return true;
if (ch == '/')return true;
if (ch == '*')return true;
if (ch == '(')return true;
if (ch == ')')return true;
return false;
}
bool is_Prim(char a, char b) {//判断操作符的优先级,若a的优先级高于b则返回true
if (a == '(' || a == ')')return true;
if (a == '*' || a == '/') {
if (b == '+' || b == '-' || b == '*' || b == '/')return true;
}
if (a == '+' || a == '-') {
if (b == '+' || b == '-')return true;
}
return false;
}
int main() {
int len = 0, r = 0;//len记录中缀表达式长度,r记录后缀表达式长度
char str[MaxSize], ch;//str字符数组记录中缀表达式
char cal[MaxSize];//cal字符数组记录后缀表达式
SqStack S;//声明一个栈
InitStack(S);//初始化栈
while (ch = getchar()) {//按位读入中缀表达式
if (ch == '\n')break;//读到换行符则结束输入
str[len++] = ch;//否则将读到的字符存入字符数组
}
for (int i = 0; i < len; ++i) {//按位扫描中缀表达式
if (!is_Op(str[i])) {//若当前位为操作数
cal[r++] = str[i];//直接将操作数放入后缀表达式
continue;
}
else {//否则当前位为操作符
if (str[i] == '(') {//若为左括号则直接压入栈中
Push(S, str[i]);
continue;
}
else if (str[i] == ')') {//若为右括号
while (!is_Empty(S)) {//则让栈中操作符依次出栈
Pop(S, ch);
if (ch == '(')break;//直到左括号为止
cal[r++] = ch;//将出栈的操作符放入后缀表达式
}
continue;
}
else {//加减乘除运算符可以用同一套逻辑
if (is_Empty(S)) {//若栈空则直接压入
Push(S, str[i]);
continue;
}
if (Query(S, ch)) {//否则需要将当前运算符与栈中运算符对比
while (!is_Empty(S)) {
//若栈非空,则需要把栈中优先级高于或等于当前运算符的所有运算符弹出
//并加入到后缀表达式中
Query(S, ch);//更新ch为栈顶运算符
if (!is_Prim(ch, str[i]))break;//若栈顶运算符优先级更低则停止出栈
Pop(S, ch);//出栈
if (ch == '(')break;//若为左括号则直接跳出循环,防止其进入后缀表达式
cal[r++] = ch;//将栈顶的运算符加入后缀表达式
}
}
Push(S, str[i]);//最后将当前操作符压入栈中
}
}
}
while (!is_Empty(S)) {//若栈中还有运算符
Pop(S, ch);//依次弹出
cal[r++] = ch;//加入后缀表达式
}
for (int i = 0; i < r; ++i)
putchar(cal[i]); //循环输出后缀表达式
putchar('\n');
return 0;
}
输入样例1
A+B-C*D/E+F
输出样例1
AB+CD*E/-F+
输入样例2
(A+(B*(C-D))-(E/F))
输出样例2
ABCD-*+EF/-
完整代码也可看我的Github:传送门
中缀转前缀的机算方法考察不多,在408考研初试中无需掌握。
(因为前缀表达式使用起来较为不便,实际应用中也不常见)
栈在递归中的应用
递归是一种重要的程序设计方法。简单来说,若在一个函数、过程或数据结构的定义中应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。
递归算法通常能把一个大型复杂问题层层转化为一个与原问题类似的规模较小的问题来求解。因此,使用递归算法时能够大大减少程序的代码量,但是相应的,其效率并不高。
需要注意的是,递归模型不能循环定义,其必须满足以下两个条件:
- 递归表达体(递归体)
- 边界条件(递归出口)
递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题。
inline int fac(int n) {//递归求阶乘(极易溢出)
if (n == 0 || n == 1)return 1;//边界条件
else return n * fac(n - 1);//递归表达式
}
inline int fib(int n) {//递归求斐波那契数列
if (n == 0)return 0;//边界条件
if (n == 1)return 1;//边界条件
return fib(n - 1) + fib(n - 2);//递归表达式
}
递归的缺点很明显:
- 太多层递归可能导致系统函数栈溢出。
- 可能包含多次重复计算。
为解决这些问题,可以将递归算法转换为非递归算法,借助栈这一数据结构即可实现这种转换。
完整代码也可看我的Github:传送门
队列的应用
树的层次遍历:从根节点开始,逐层遍历,每遍历一层结点,先将其插入队列,然后逐个扫描,并按顺序把每个结点的孩子结点也按顺序插入队列,之后再让这个结点出列,完成对该结点的扫描,依此类推。(到相应章节会有详细讲解)
图的广度优先搜索:从一个结点开始,查找相邻结点是否被遍历过。(会在“图”的章节详细学习)
队列在计算机系统中的应用
- 解决主机与外部设备之间速度不匹配的问题
Eg:打印数据缓冲区 - 解决由多用户引起的资源竞争问题:
多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用策略台(可用队列实现)。
这一章的东西还是很多的,而且是后面图论和排序算法学习的重要基础,需要反复学习,巩固记忆。
以上。