栈的特点 LIFO 后进先出 使得它在处理一些相关问题的时候非常强大
1、逆序输出(conversion)
特点:1.输出与处理过程相反 2.递归深度、输出长度不易预知
典型问题: 进制转换
2、递归嵌套
特点:1.具有自相似的问题可递归描述 2.分支和位置不确定
典型问题: 括号匹配
3、延迟缓冲
特点:线性扫描算法中要预读足够长才能够处理前缀
典型问题: 中缀表达式
4、RPN (逆波兰表达式) 基于栈结构的特定计算模式
—— 清华大学 邓俊辉《数据结构》
这里就举一道 括号匹配 和 布尔表达式求值 的题目作为例子,尝试利用栈结构来解决一些问题。
括号匹配
对于给定的字符串 只包含( { [ ] } ) 括号可以嵌套使用
合法如 “((){}[{}])” 则输出Yes 非法如 “((){}[)” 则输出 No
对题目进行初步分析后,我们想,怎么才能缩小问题的规模呢?
对于给定的一串括号 我们发现
遇到右括号时如果存在对应的左括号 则可以消去这对括号 原问题的合法性和其消去一对括号的合法性是相同的
比如说 如果”((){})”合法 那么 “({})”也必然是合法 那么”()”合法 就回归到一个最基本的情形了
我们通过消去匹配的括号,来缩小问题规模
也就是 减而治之(Decrease and Conquer) 的策略
利用栈结构,我们对输入的字符串进行扫描
如果我们遇到一个左括号,则令其入栈,遇到右括号,则令栈顶弹出。
如果不匹配,则终止扫描,输出No;如果匹配,弹出栈顶的左括号,就达到了消去括号的目的。
如果最后整个栈为空栈,则证明所有的括号都成功消去,即匹配。
以下是C语言实现的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//括号匹配 通过栈来实现
//将所有的左括号 压入栈 然如果碰到右括号 则令左括号弹出
...
//为节省篇幅 省略定义栈结构相关代码
char pop(Stack* ps){ //出栈 并且返回出栈的节点的数据 这里的数据域是char类型
if(ps->size == 0){
printf("Empty stack!");
return ;
}else{
ps->size -- ;
char ret = ps->top->data;
Node* temp = ps->top ;
ps->top = ps->top->next;
free(temp);
temp = NULL ;
return ret ;
}
}
int isEmpty(Stack* ps){ // 判断栈是否为空栈
return ps->size > 0 ? 0 : 1;
}
int checkPro(Stack* stack,char * s ){ //检测括号匹配
int i ;
char c ;
for( i = 0 ; i <strlen(s); i++){
if(s[i] == '('){
push(stack,'s'); // 小括号 对应的数据为 's' 入栈
}else if (s[i] == '['){
push(stack,'m');// 中括号 对应的数据为 'm' 入栈
}else if(s[i] == '{'){
push(stack,'l');// 大括号
}else if(s[i] == ')' && !isEmpty(stack)){ //碰到右小括号 且 栈不为空
c = pop(stack); // 栈顶出栈
if( c != 's'){ // 弹出的栈 不匹配
return 0;
}
}else if(s[i] == ']' && !isEmpty(stack)){//同小括号
c = pop(stack);
if( c != 'm'){
return 0;
}
}else if(s[i] == '}' && !isEmpty(stack)){//同小括号
c = pop(stack);
if( c != 'l'){
return 0;
}
}
}
return isEmpty(stack); // 如果扫描进行结束没有退出 栈为空则匹配 非空则说明有多余的左括号
}
int main(){
Stack stack ;
init(&stack);
char s[100];
scanf("%s",&s);
if(checkPro(&stack,s)){
printf("yes");
}else{
printf("No");
}
return 0;
}
|
Boolean Expressions
[POJ Boolean Expressions][1]
[1]: http://poj.org/problem?id=2106
利用栈结构进行表达式求值,主要实现的思路是:
1、操作数与值分离,用独立的两个栈来存放操作数和值
2、判断操作数的优先级,一般来说,如果当前操作数优先级低于栈顶操作数的优先级,则说明栈顶的操作数可以执行了,就弹出操作数栈顶和相应的值栈,进行运算,并且将运算结果重新压入值栈中
这一题布尔表达式求值 实际上操作数只有 () ! | & 五个
(优先级最高 !次之 &再次 然后是| )可以定义为最低
然后因为这题值栈事实上就是V/F 操作数栈也不过5个操作数 所以可以用两个数组来进行模拟简单的栈
又由于!的优先级是很高的 所以我们可以把 !V !F 看做是一个值 来进行压入值栈的操作
计算只要负责 | 和 & 就可以了
以下是借鉴自某CSDN博客的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#include<cstdio>
#define MAX 150
bool val[MAX]; // 值栈
int op[MAX]; //操作数栈
int vp, pp; // 栈顶下标
/* 定义优先级
(: 0
| : 1
& : 2
! : 3
) : 4
*/
void insert(bool b){ //入栈操作
while( pp && (op[pp-1] == 3) ){ // 处理!操作 如果操作数栈优先级为3 则说明为!操作
b = ! b; // 取反
--pp; // 栈顶下标-1 相当于 弹出栈顶操作数
}
val[vp++] = b; // 将值压入栈中 并且vp++ 栈顶下标+1
}
void calc(){ // 计算操作
bool b = val[--vp]; // 取出一个值
bool a = val[--vp];
int opr = op[--pp]; //取出一个操作数 这里的pp 和 vp 原来都是比实际栈顶下标大 1的
insert(opr == 1 ? a | b : a & b ); // 压入结果 如果操作数为1即为 | 不为1 则为 &
}
int main(){
int cnt = 0; // 计算第几个表达式的值
char c ;
while( ~(c = getchar()) ){ // 这是个小技巧 EOF = -1 按位取反 等价于 (c = getchar()) != EOF
vp = pp = 0 ; // 初始化下标为 0 pp也代表着操作数的数量
do{
if( c == '('){ //左括号 压入栈中
op[pp++] = 0 ;
}else if( c == ')'){
while(pp && op[pp-1]){ //处理左括号以前所有的操作 处理完之后相当于消去了()
calc();
}
--pp; // pp-- 相当于弹出 )
insert(val[--vp]); // 如果表达式为 !() 形式 还要计算一次!
}else if( c == '!'){
op[pp++] = 3; //操作数 ! 入栈
}else if( c == '|'){
while( pp && op[pp-1] >= 1){ // 计算|之前 优先级比|高的操作
calc();
}
op[pp++] = 1; // 压入 | 操作
}else if( c == '&'){
while( pp && op[pp-1] >= 2){ // 计算&之前 优先级比&高的操作
calc();
}
op[pp++] = 2 ;// 压入 & 操作
}else if( c == 'V' || c == 'F'){
insert(c == 'V' ? true : false); //压入 true or false
}
// 空格被忽略
}while((c = getchar())!= '\n' && ~c); // ~c 等价于 c != EOF
while(pp){ // 如果还存在操作数 继续计算
calc();
}
//输出结果
printf("Expression %d: %c\n",++cnt,(val[0] ? 'V' : 'F'));
}
return 0;
}
|
这个代码写的我个人觉得非常的优雅,
首先是通过将运算符分优先级后编号,然后就利用两个数组来实现值栈和操作数栈,简洁而有效。
然后是两个pp vp 操作数下标 和 值的下标 既作为栈顶 又作为操作数的数量 实际写的过程中 ++ – 其实是很容易搞错的
并且将 !的计算放在了 calc()的函数中 这样就可以同等对待剩余的 | 和 &
还有特殊的 !()模式的处理 以及 剩余操作数的 处理
非常值得学习的一段代码,努力把它消化了
总结一下:
栈结构很强大,LIFO的特点使得它在表达式求值、相关匹配的算法题中起到很大作用。
但也不一定要拘泥于栈结构的形式,数组也可以作为栈。而且一般向量(数组)作为栈的
会以向量尾部作为栈顶,因为这样出栈的操作时间复杂度就是O(1) 而如果在头部, 则是O(n) 和向量长度成正比,值得注意。