栈的性质是后进先出。
- 关键要明白何时出栈、何时入栈
- 出栈后下一步如何进行
一、数制转换
十进制数N转其他d进制的转换原理:N = (N div d) × d + N mod d(其中:div 为整除运算,mod为求取余运算)
例如:(1348)10=(2504)8,运算过程如下表:
N | N div 8 | N mod 8 |
---|---|---|
1348 | 168 | 4 |
168 | 21 | 0 |
21 | 2 | 5 |
2 | 0 | 2 |
上述计算过程是从低位到高位顺序产生八进制的各个数位,而打印输出,一般来说应从高位到低位进行,恰好和计算过程相反,符合栈后进先出的性质。
void conversion(){
//对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数
InitStack(S);//构造空栈
scanf("%d",&N);
while(N){
Push(S, N % 8);//将余数进栈
N = N / 8;
}
while(!StackEmpty(S)){//栈不为空
Pop(S, e);//出栈
printf("%d",e);
}
}
例题
char * convertToBase7(int num){
int str[20];//栈
int n = 0,flag = 0;
if(num == 0)//防止num=0的情况返回为空
str[n++] = 0;
if(num < 0){//记录是否为负数
flag = 1;
num = -num;
}
while(num){//转换为7进制
str[n++] = num % 7;
num /= 7;
}
char *count = (char*)malloc(sizeof(char)*1000);
int m = 0;
if(flag)//负数在第一位方‘-’
count[m++] = '-';
while(n > 0){//反转
count[m++] = '0' + str[n-1];
n--;
}
count[m] = '\0';
return count;
}
二、括号匹配检验
假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序随意,即 ( [ ] ( ) )或[ ( [ ] [ ] ) ]等为正确的格式,[ ( ] )或( [ ( ) )或( ( ) ]均为不正确的格式。检验括号是否匹配的方法可用“期待的急迫程度”这个概念来描述。例如考虑下列括号序列:
[ ( [ ] [ ] ) ]
12345678
当计算机接受了第一个括号后,它期待着与其匹配的第八个括号的出现,然而等来的却是第二个括号,此时第一个括号“ [ ”只能暂时靠边,而迫切等待与第二个括号相匹配的、第七个括号“)”的出现,类似地,因等来的是第三个括号“[”,其期待匹配的程度较第二个括号更急迫,则第二个括号也只能靠边,让位于第三个括号,显然第二个括号的期待急迫性高于第一个括号;在接受了第四个括号之后,第三个括号的期待得到满足,消解之后,第二个括号的期待匹配就成为当前最急迫的任务了,……,依次类推。可见,这个处理过程恰与栈的特点相吻合。由此,在算法中设置一个栈,每读入一个括号,若是右括号,则或者使置于栈顶的最急迫的期待得以消解,或者是不合法的情况;若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消解的期待的急迫性都降了一级。另外,在算法的开始和结束时,栈都应该是空的。
例题
bool isValid(char * s){
int n = strlen(s);//获取字符串长度
char str[10010];//栈
int num = 0;//栈的尾标记
for(int i=0;i<n;i++){
if(s[i] == '(' || s[i] == '[' || s[i] == '{')//左符号进栈
str[num++] = s[i];
else{
if(s[i] == ')'){//右括号
if(num<1 || str[num-1] != '(')//若栈为空或者栈尾不是左括号,则匹配失败
return false;
else//出栈
num--;
}
else if(s[i] == ']'){
if(num<1 || str[num-1] != '[')//若栈为空或者栈尾不是左括号,则匹配失败
return false;
else//出栈
num--;
}
else{
if(num<1 || str[num-1] != '{')//若栈为空或者栈尾不是左括号,则匹配失败
return false;
else//出栈
num--;
}
}
}
if(num == 0)//栈为空
return true;
else//栈不为空
return false;
}
三、迷宫求解
求迷宫中从入口到出口的所有路径是一个经典的程序设计问题。由于计算机解迷宫时,通常用的是“穷举求解”的方法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路返回,换一个方向再继续探索,直至所有可能的通路都探索到为止。为了保证在任何位置上都能沿原路退回,显然需要用一个后进先出的结构来保存从入口到当前位置的路径。因此,在求迷宫通路的算法中应用“栈”也就是自然而然的事了。
大部分迷宫问题一般使用广度优先搜索(BFS)和深度优先搜索(DFS) 解答。
使用栈求解与使用广度优先搜索(BFS)和深度优先搜索(DFS)不同的是,栈不仅需要记录路径,还需要记录点下一步的方向,以便于返回上一点时可以接着原来的没有进行的方向前进,否则会出现死循环无法结束。
假设“当前位置”指的是“在搜索过程中某一时刻所在图中某个方块位置”,则求迷宫中一条路径的算法的基本思想是:若当位置“可通”,则纳入“当前路径”,并继续朝“下一位置”探索,即切换“下一位置”为“当前位置”,如此重复直至到达出口;若当前位置“不可通”,则应顺着“来向”退回到“前一通道块”,然后朝着除“来向”之外的其他方向继续探索;若该通道块的四周 4 个方块均“不可通”,则应从“当前路径”上删除该通道块。 所谓”下一位置“指的是“当前位置”四周 4 个方向(东、南、西、北)上相邻的方块。 假设以栈 S 记录“当前路径”,则栈顶中存放的是“当前路径上最后一个通道块”。由此,“纳入路径”的操作即为“当前位置入栈”;“从当前路径上删除前一通道块”的操作即为“出栈”。
设定当前位置的初值为入口位置;
do{
若当前位置可通,
则{
将当前位置插入栈顶; //纳入路径
若该位置是出口位置,则结束; //求得路径存放在栈中
否则切换当前位置的东邻方块为新的当前位置;
}
否则{
若栈不空且栈顶位置尚有其他方向未经探索,
则设定新的当前位置为沿顺时针方向旋转找到的栈顶位置的下一个相邻块;
若栈不空但栈顶位置的四周均不可通,
则{
删去栈顶位置; //从路径中删去该通道块
若栈不空,则重新测试新的栈顶位置,
直至找到一个可通的相邻块或出栈至栈空;
}
}
} while(栈不空)
在此,尚需说明一点的是,所谓当前位置可通,指的是未曾走到过的通道块,即要求该方块位置不仅是通道块,而且既不在当前路径上(否则所求路径就不是简单路径),也不是曾经纳入过路径的通道块(否则只能在死胡同内转圈)。
例题
思路:遍历所有路径,查询是否可以到达终点,可以到达终点则记录。
#include<stdio.h>
int main()
{
int n,m,t;
scanf("%d %d %d",&n,&m,&t);
int sx,sy,fx,fy;
scanf("%d %d %d %d",&sx,&sy,&fx,&fy);
int atlas[50][50] = {0};
while(t --){
int a,b;
scanf("%d %d",&a,&b);
atlas[a][b] = 1;
}
int num = 0, route[1000][3], cnt = 0, flag[50][50] = {0};//route路径记录,cnt路径记录下标,num路径的数量,flag标记是否已经走过该点
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//四个方向偏移量
route[cnt][0] = sx;//将起始点加入路径
route[cnt][1] = sy;
route[cnt][2] = 0;//方向置为0
flag[sx][sy] = 1;//标记起点已经走过
cnt ++;//路径下标+1
while(cnt > 0){//路径不为空
int x = route[cnt-1][0], y = route[cnt-1][1];
if(route[cnt-1][2] >= 4 || (x == fx && y == fy)){//若四个方向或者是终点,则返回路径上一个点
flag[x][y] = 0;//将该点置为未走过
cnt--;//返回路径上一个点
continue;//跳过本次循环
}
for(;route[cnt-1][2]<4;route[cnt-1][2]++){//从该点未搜索的方向进行
int xx = x + dx[route[cnt-1][2]], yy = y + dy[route[cnt-1][2]];
if(xx > 0 && xx <= n && yy > 0 && yy <= m && atlas[xx][yy] == 0 && flag[xx][yy] == 0){//该点在范围内,不是障碍,路径未走过
route[cnt-1][2]++;//换下一个方向
route[cnt][0] = xx;//该点进入队列
route[cnt][1] = yy;
route[cnt][2] = 0;//方向置为起始方向
cnt++;//路径+1
if(xx == fx && yy == fy){//若为终点,则数量+1
num ++;
}
flag[xx][yy] = 1;//该点置为已经走过
break;//跳过方向循环
}
}
}
printf("%d",num);
return 0;
}
四、表达式求值
表达式求值是栈应用的一个典型例子。这里介绍一中简单直观、广为使用的算法,通常称为“算符优先法”。
首先了解算术四则运算的规则。即:
- 先乘除,后加减;
- 从左算到右;
- 先括号内,后括号外。
由此,4+2×3-10/5的计算顺序应为
4+2×3-10/5 = 4+6-10/5 = 10-10/5 = 10-2 = 8
算法优先法就是根据这个运算优先关系的规定来实现对表达式的编译或解释执行的。
任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成的,我们称它们为单词。一般地,操作数既可以是常数也可以是被说明为变量或常量的标识符;运算符可以分为算术运算符、关系运算符和逻辑运算符3类;基本界限符有左右括号和表达式结束符等。为了叙述的简洁,我们仅讨论简单算术表达式的求值问题。这种表达式只含加、减、乘、除4种运算符。读者不难将它推广到更一般的表达式上。
根据上述3条运算规则,在运算的每一步中,任意两个相继出现的算符θ1和θ2之间的优先关系至多是下面3种关系:
- θ1<θ2 θ1的优先权低于θ2
- θ1=θ2 θ1的优先权等于θ2
- θ1>θ2 θ1的优先权高于θ2
+ | - | * | / | ( | ) | # | |
---|---|---|---|---|---|---|---|
+ | > | > | < | < | < | > | > |
- | > | > | < | < | < | > | > |
* | > | > | > | > | < | > | > |
/ | > | > | > | > | < | > | > |
( | < | < | < | < | < | = | |
) | > | > | > | > | > | > | |
# | < | < | < | < | < | =(结束) |
为实现算符优先算法,可以使用两个工作栈。一个称做OPTR,用以寄存运算符;另一个称做OPND,用以寄存操作数或运算结果。算法的基本思想是:
(1)首先置操作数栈为空栈,表达式起始符“#”为运算符栈的栈底元素;
(2)依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符均为“#”)。
OperandType EvaluateExpression(){
//算术表达式求值的算符优先算法。设OPTR和OPND分别为运算符栈和运算数栈
//OP为运算符集合
InitStack(OPTR);//创建空栈OPTR
Push(OPTR, '#');//OPTR栈压入‘#’
InitStack(OPND);//创建空栈OPND
c = getchar();//获取输入一个字符
while(c != '#' || GetTop(OPTR) != '#'){//#相遇,运算结束
if(!In(c, OP)){//是否为运算符
Push(OPND, c);//不是运算符则进运算数栈OPND
c = getchar();//获取下一个输入
}
else{
switch(Precede(GetTop(OPTR), c)){//比较获取的运算符与符号栈顶符号的优先级关系,Precede获取优先级函数
case '<': //栈顶元素优先权低
Push(OPTR, c);//压入操作符栈OPTR
c = getchar();//获取下个输入
break;
case '=': //脱括号并接收下一个字符
Pop(OPTR, x);//出栈OPTR
c = getchar();
break;
case '>': //退栈两个数进行运算,再将结果入栈
Pop(OPTR, theta);//获取操作符号theta
Pop(OPND, b);//获取运算数b
Pop(OPND, a);//获取运算数a
Push(OPND, Operate(a, theta, b));//对a、b进行theta运算结果入栈,Operate运算函数
break;
}
}//switch
}//while
return GetTop(OPND);
}//EvaluateExpression
例题
思路:与上面的伪代码一样的思路
#include<stdio.h>
#include<string.h>
char OPTR[100010], str[100010];
int OPND[100010], tr, nd;
char Precede(char a, char b){//优先级获取
// 开二维数组存表
char priority[7][7]={
{'>','>','<','<','<','>','>'},// +
{'>','>','<','<','<','>','>'},// -
{'>','>','>','>','<','>','>'},// *
{'>','>','>','>','<','>','>'},// /
{'<','<','<','<','<','=','0'},// (
{'>','>','>','>','0','>','>'},// )
{'<','<','<','<','<','0','='} // #
};
int i,j;
switch(a){
case'+':i=0;break;
case'-':i=1;break;
case'*':i=2;break;
case'/':i=3;break;
case'(':i=4;break;
case')':i=5;break;
case'#':i=6;break; // # 是表达式的结束符
}
switch(b){
case'+':j=0;break;
case'-':j=1;break;
case'*':j=2;break;
case'/':j=3;break;
case'(':j=4;break;
case')':j=5;break;
case'#':j=6;break;
}
return priority[i][j];
}
int Operate(int a, char c, int b){//运算符计算
switch(c){
case '+':
return a + b;
case '-':
return a - b;
case '*':
return a * b;
case '/':
return a / b;
}
}
int main()
{
OPTR[tr++] = '#';
scanf("%s",str);
str[strlen(str)] = '#';
for(int i=0;i<strlen(str);){
if(str[i]>='0' && str[i]<='9'){
int a = str[i++] - '0';
while(str[i]>='0' && str[i]<='9'){//数字存在多位,获取所有数字
a = a * 10 + str[i++] - '0';
}
OPND[nd++] = a;
}
else{
char c;
int a,b;
switch(Precede(OPTR[tr-1], str[i])){
case '<':
OPTR[tr++] = str[i++];
break;
case '=':
tr--;
i++;
break;
case '>':
c = OPTR[--tr];
b = OPND[--nd];
a = OPND[--nd];
OPND[nd++] = Operate(a, c, b);
break;
}
}
}
printf("%d",OPND[--nd]);
return 0;
}
五、背包问题
例题
就是求出最多可以放多大的空间。
#include<stdio.h>
#include<math.h>
int n,v;
int cnt,flag[40],num[40];
void stack(){
int t = 1, a = 0;
while(t > 0){
if(a > v){//空间超过则该物品不能放入
t --;//返回该层
a -= num[flag[t]];//去除该物品
continue;
}
if(a > cnt)//更新空间最大值
cnt = a;
if(flag[t] >= n){//该层的物品全部试过了
t --;//返回下一层
a -= num[flag[t]];//去除该物品
continue;
}
a += num[++flag[t]];//将物品加入
t ++;//去下一层
flag[t] = flag[t-1];//下一层,从该物品后拿物品
}
}
int main()
{
scanf("%d",&v);
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&num[i]);
stack();
printf("%d",v-cnt);
return 0;
}