文章目录
栈的应用
1.括号匹配
判断括号是否是两两匹配的。
遇到左括号就入栈。遇到右括号就出栈一个左括号。
1.1三种错误情况
匹配失败:
空栈:
最后栈非空:
1.2流程图
❗1.3算法c实例
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#define MaxSize 50 //定义栈中元素最大个数
typedef char ElemType; //定义栈中元素类型为char
typedef struct{
char data[MaxSize];
int top;
}SqStack;
void InitStack(SqStack &S);
bool StackEmpty(SqStack S);
bool StackFull(SqStack S);
void Push(SqStack &S,ElemType x);
void Pop(SqStack &S,ElemType *x);
//匹配算法
bool bracketCheck(ElemType str[], int length){
SqStack S;
InitStack(S); //初始化栈
for(int i=0; i<length; i++){
//扫描到左括号就入栈
if(str[i]=='('||str[i]=='{'||str[i]=='['){
Push(S,str[i]);
}
else{
//扫描到右括号并且当前栈为空,即右括号单身情况
if(StackEmpty(S)){
return false; //匹配失败
}
ElemType topElem; //用来保存弹出栈的栈顶元素
Pop(S, &topElem); //栈顶元素出栈
if(str[i]==')' && topElem!='('){
return false;
}
if(str[i]=='}' && topElem!='{'){
return false;
}
if(str[i]==']' && topElem!='['){
return false;
}
}
}
//扫描完整个字符串,如果栈不为空,说明左括号单身
return StackEmpty(S);
}
int main(){
char str[MaxSize];
printf("请输入需要判断的括号:\n");
scanf("%s",str);
int len = strlen(str);
printf("当前输入的括号个数为:%d\n",len);
printf("--------现在开始进行判断--------\n");
if(bracketCheck(str,len)){
printf("匹配成功!");
}else{
printf("匹配失败!");
}
return 0;
}
//初始化栈
void InitStack(SqStack& S){
S.top = -1;
}
//判断栈是否为空
bool StackEmpty(SqStack S){
if(S.top == -1){
return true;
}
return false;
}
//判断栈满
bool StackFull(SqStack S){
if (S.top == MaxSize-1) //top的位置值等于MaxSize-1时栈满,因为是从0开始的
return true;
else
return false;
}
//入栈
void Push(SqStack &S,ElemType x){
if(StackFull(S)){
printf("栈已满"); //栈已满
return;
}
S.top += 1;
S.data[S.top] = x;
}
//出栈,用x返回
void Pop(SqStack &S,ElemType *x){
if(StackEmpty(S)){
printf("栈为空");
return;
}
*x = S.data[S.top];
S.top -= 1;
}
❗2.表达式求值
- 三种算术表达式:
- 中缀表达式
- 后缀表达式
- 前缀表达式
- ❗后缀表达式の考点
- 中缀表达式转后缀表达式
- 后缀表达式求值
- 前缀表达式の考点
- 中缀表达式转前缀表达式
- 前缀表达式求值
算术表达式由三部分组成:操作数、运算符、界限符。
(
1
+
2
)
×
4
(1+2)×4
(1+2)×4
- 操作数(运算数):就是1、2、4这些进行运算的数字
- 运算符:就是+、×这些运算
- 界限符:就是符号()这种进行运算顺序的限制。
反应运算的运算顺序。
而不用界限符也能无歧义地表达运算顺序,那就有:
- Reverse Polish notation (逆波兰表达式 = 后缀表达式)
- Polish notation (波兰表达式=前缀表达式)
中缀表达式 | 后缀表达式 | 前缀表达式 |
---|---|---|
运算符在两个操作数中间 | 运算符在两个操作数后面 | 运算符在两个操作数前面 |
a+b | ab+ | +ab |
a+b-c | ab+c- | -+abc |
a+b-c(先算b-c) | a bc- + | + a -bc |
a+b-c*b | ab+ cd* - | - +ab *cd |
2.1表达式求值
表达式求值要解决的问题一般是输入一个字符串表示的表达式,要求输出它的值。当然也有变种比如表达式中是否包含括号,指数运算,含多少变量,判断多个表达式是否等价,等等。
表达式一般需要先进行语法分析(grammer parsing)再求值,也可以边分析边求值,语法分析的作用是检查输入的字符串是否是一个合法的表达式,一般使用语法分析器(parser)解决。
表达式包含两类字符:运算数和运算符。对于长度为 n 的表达式,借助合适的分析方法,可以在 O(n) 的时间复杂度内完成分析与求值。
2.2中缀转后缀(手算)
中缀转后缀的手算方法:
- 确定中缀表达式中各个运算符的运算顺序。
- 选择下一个运算符,按照「左操作数,右操作数,运算符」的方式组合成一个新的操作数。
- 如果还有运算符没被处理,就继续步骤②
一个中缀表达式中的运算,在逻辑上有多种顺序(运算顺序不唯一),但是在后缀表达式中只有一种表示。
所以,在算法中想要验证(手算)是否正确,只能使用前面的那种后缀表达式逻辑。(保证算法的确定性)
所以,在中缀转后缀中,要采用**“左优先”原则**:只要左边的运算符能先运算,那么就先优先计算左边的。
要注意下面这个例题:
A+B可以在不干扰 * / 运算的情况下,先运算,所有算是左边能先运算的运算符。
A
+
B
−
C
∗
D
/
E
+
F
↓
A
B
+
C
D
∗
E
/
−
F
+
A+B-C*D/E+F\\ ↓\\ AB+ CD*E/ - F+
A+B−C∗D/E+F↓AB+CD∗E/−F+
2.3后缀表达式计算
手算:从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数。
机算:
- 从左往右扫描下一个元素,直到处理完所有元素。
- 若扫描到操作数则压入栈,并回到①;否则执行③。
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①。
2.4中缀转前缀
中缀转前缀的手算方法:
- 确定中缀表达式中各个运算符的运算顺序。
- 选择下一个运算符,按照「运算符,左操作数,右操作数」的方式组合成一个新的操作数。
- 如果还有运算符没被处理,就继续②右边
“右优先”原则:只要右边的运算符能先计算,就优先算的。
A
+
B
∗
(
C
−
D
)
−
E
/
F
↓
+
A
−
∗
B
−
C
D
/
E
F
A+B*(C-D)-E/F\\ ↓\\ +A- *B -CD /EF
A+B∗(C−D)−E/F↓+A−∗B−CD/EF
2.5中缀转后缀(机算)
3.3.2_栈在表达式求值中的应用(下)_哔哩哔哩_bilibili
中缀转后缀表达式的机算方法:
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾。可能遇到三种情况:
-
遇到操作数。直接加入后缀表达式。
-
遇到界限符。
- 遇到
(
直接入栈; - 遇到
)
则依次弹出栈内运算符并加入后缀表达式,直到弹出(
为止。
注意:
(
不加入后缀表达式。 - 遇到
-
遇到运算符。
- 依次弹出栈中已存在的优先级高于或等于当前运算符的所有运算符,并加入后缀表达式;
- 若碰到
(
或栈空则停止。之后再把当前运算符入栈。
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
最后:
通过中缀表达式转后缀表达式的机算方法、后缀表达式的计算,这两种方式的结合,可以直接对中缀表达式进行运算,判断完一个运算符的优先性之后,直接进行运算。
2.5表达式树与逆波兰表达式
一种递归分析表达式的方法是,将表达式当成普通的语法规则进行分析,分析后拆分成如图所示的表达式树,然后在树结构上自底向上进行运算。
对这棵表达式树进行的:
前序遍历 --> 前缀表达式
中序遍历 --> 中缀表达式(需要加界限符)
后序遍历 --> 后缀表达式
3.进制转换
对十进制数进行取余之后放入栈中,再出栈(倒序)就是目标结果。
#include<iostream>
#include<Windows.h>
using namespace std;
#define MaxSize 100 //栈最大可以存放的元素个数
typedef char ElemType; //顺序栈存储的数据类型、代替ElemType
//创建顺序栈
typedef struct
{
ElemType* base; //栈底指针
int top; //栈顶的位置 如 0、1、2、3、4....MaxSize
} SqStack; //顺序栈的结构体定义
bool InitStack(SqStack& stack); //初始化栈
bool StackEmpty(SqStack stack);//判断是否为空
bool StackFull(SqStack stack); //判断是否已满
int GetStackSize(SqStack& stack);//获取顺序栈中元素个数
bool Push(SqStack& stack, ElemType value);//入栈
bool Pop(SqStack& stack, ElemType& value);//出栈
bool Pop(SqStack& stack); //出栈重载
bool GetTop(SqStack& stack, ElemType& value);//获取栈顶的元素
void DestroyStack(SqStack& stack);//销毁栈、释放栈的内存
//初始化顺序栈
bool InitStack(SqStack& stack){
//注意:这里使用new进行空间分配,所以在后面摧毁栈的时候需要delete释放空间
//动态分配一个ElemType类型MaxSize长度的空间,将地址给顺序栈Stack的栈底指针
stack.base = new ElemType[MaxSize];
//判断顺序栈的栈底指针(stack.base)是否为空,若无地址,则分配失败
if(!stack.base){
return false;
}
stack.top = -1; //初始化栈顶指针的位置为-1
return true;
}
//判断栈空
bool StackEmpty(SqStack stack){
if (stack.top == -1)
return true;
else
return false;
}
//判断栈满
bool StackFull(SqStack stack){
if (stack.top == MaxSize-1) //top的位置值等于MaxSize-1时栈满,因为是从0开始的
return true;
else
return false;
}
//顺序栈中元素个数
int GetStackSize(SqStack& stack){
return stack.top+1; //栈顶位置即top的数值,就是栈中元素的个数
}
/**
* @brief 顺序栈入栈:
* 开辟一个新的空间,栈顶+1,然后将数据存入stack.base[stack.top]所在的位置.
*
* @param stack
* @param value
* @return true
* @return false
*/
bool Push(SqStack& stack, ElemType value){
if (StackFull(stack)){
cout<<"栈满"<<endl;
return false;
}
//若栈未满,执行入栈操作
stack.top++; //栈顶自增1
stack.base[stack.top] = value; //以栈顶位置作为下标存储数据
return true;
}
/**
* @brief 顺序栈出栈:
* 读取栈顶stack.base[stack.top]的元素,然后使栈顶-1.
*
* @param stack
* @param value
* @return true
* @return false
*/
bool Pop(SqStack& stack, ElemType &value){
if (StackEmpty(stack)){
cout<<"栈为空"<<endl;
return false;
}
value = stack.base[stack.top]; //以栈顶位置作为下标的值赋值给value返回
stack.top--; //栈顶自减1
return true;
}
// 重载
bool Pop(SqStack& stack){
if (StackEmpty(stack)){
cout<<"栈为空"<<endl;
return false;
}
stack.top--; //栈顶自减1
return true;
}
//读取栈顶元素
bool GetTop(SqStack& stack, ElemType &value){
if (StackEmpty(stack)){
cout<<"栈为空"<<endl;
return false;
}
value = stack.base[stack.top];
return true;
}
//销毁栈、释放栈的内存
void DestroyStack(SqStack& stack){
if(stack.base) { //若栈底指针分配有地址,则释放
delete stack.base; //释放栈底指针的地址
stack.top = -1; //令栈顶位置为0
stack.base = NULL; //将栈底指针指向空
cout<<"栈已释放内存!"<<endl;
}
}
//-------------------------------------------------------------------------------
// 十进制 -> 二进制
void DTB(int num, SqStack& Stack)
{
char num_b[64]; //用来存放转换出来的二进制
int i = 0;
while(num) { //2进制,入栈
// !!!注意:将余数转化为字符,用 +'0'
Push(Stack, num%2+'0'); //余数放入栈
num /= 2;
}
while(!StackEmpty(Stack)) { //出栈,直到栈为空
Pop(Stack, num_b[i++]);
}
cout<<"该数对应的二进制数为:"<<num_b<<endl;
}
// 八进制
void DTO(int num, SqStack& Stack)
{
char num_o[32]; //用来存放转换出来的八进制
char* temp = num_o;
while(num) { //入栈
Push(Stack, num%8+'0');
num /= 8;
}
while(!StackEmpty(Stack)) { //出栈 直到栈为空
Pop(Stack, *temp);
temp++; //下一个位置
}
*temp = '\0';
printf("该数对应的八进制数为:%s\n",num_o);
}
// 十六进制
void DTH(int num, SqStack& Stack)
{
char num_h[12] ; //用来存放转换出来的16进制
char* temp = num_h;
char top_num;
while(num) //入栈
{
Push(Stack,num%16);
num /= 16;
}
while(!StackEmpty(Stack)) //出栈 直到栈为空
{
Pop(Stack, top_num);
if((int)top_num > 9)
*temp = top_num - 10 + 'A';
else
*temp = top_num + '0';
temp++;
}
*temp = '\0';
printf("该数对应的十六进制数为:%s\n",num_h);
}
int main()
{
SqStack stack; //创建顺序栈
InitStack(stack); //初始化顺序栈
int value = 12;
DTB(value,stack);
DTO(value,stack);
DTH(value,stack);
//释放栈的内存
DestroyStack(stack);
system("pause");
return 0;
}
4.递归
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈,存储:
- 调用返回地址
- 实参
- 局部变量
适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题。
递归调用时,函数调用栈可称为“递归工作栈”。
每进入一层递归,就将递归调用所需信息压入栈顶。每退出一层递归,就从栈顶弹出相应信息。
缺点
- 递归运算越多,空间复杂度越高。太多层递归可能会导致栈溢出。
- 效率低。可能包含很多重复计算。
解决办法:
- 可以自定义栈,将递归算法改成非递归算法。
- 可以使用带备忘录(memo)的递归(动态规划)。