第四章 栈与队列
(a)栈接口与实现
Stack()
empty()
push(5)
pop()
size()
top()
实现:由向量派生
template <typename T> class Stack: public Vector<T>{
public: //size()、empty()以及其他开放接口均可直接沿用
void push(T const & e){insert(size(),e);} //入栈
T pop(){return remove(size() - 1);} //出栈
T & top() {return (*this)[size() - 1];} //取顶
};//以向量为首/末端为栈底/顶
(c1)栈应用:进制转换
典型应用场合
1、逆序输出(conversion):输出次序与处理过程颠倒;递归深度和输出长度不易预知;
2、递归嵌套(stack permutation + parenthesis):具有自相似性的问题可递归描述,但分支位置和嵌套深度不固定;
3、延迟缓冲(evaluation):线性扫描算法模式中,在预读足够长之后,方能确定可处理的前缀;
4、栈式计算(RPN):基于栈结构的特定计算模式。
算法实现
void convert( Stack<char> & S,__int64 n, int base){
static char digit[] = //新进制下的数位符号,可视base取值范围适当扩充
{'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
while ( n > 0 ){//由低到高,逐一计算新进制下各数位
S.push( digit[n % base]); //余数(对应的数位)入栈
n /= base; //n更新为其对base的除商
}
}
main(){
Stack<char> S; convert(S, n, base);//用栈记录转换得到的各数位
while(!S.empty()) printf("%c",S.pop());//逆序输出
}
(c2)栈应用:括号匹配
尝试:(0)平凡情况。无括号的表达式是匹配的;(1)减而治之;(2)分而治之。(1和2均为必要性,可以举出反例,要充分借助充分性)
构思:消去一对紧邻的左右括号,不影响全局的匹配判断。
Q:如何找到这对括号?如何使问题简化?
A:顺序扫描表达式,用栈记录已扫描的部分,反复迭代,遇到“(”则进栈,遇到“)”则出栈。
实现
bool paren(const char exp[], int lo, int hi){//exp[lo,hi)
Stack<char> S; //使用栈记录已发现但尚未匹配的左括号
for (int i = lo; i < hi; i++)//逐一检查当前字符
if('('==exp[i]) S.push(exp[i]); //遇左括号,则进栈
else if(!S.empty()) S.pop(); //遇右括号:若占非空,则弹出左括号
else return false; //否则(遇右括号时栈已空),则不匹配
return S.empty(); //最终,栈空当且仅当匹配
}
以上思路及算法,可以便捷推广至多种括号并存的情况。
Q:能否使用多个计数器?
A:不行。反例:[ ( ] )
只需约定“括号”的通用格式,而不必事先固定括号的类型与数目。
(c3)栈应用:栈混洗
考查栈A=<a1,a2,…an](左端为栈顶),B=S=∅,,只允许将A的顶元素弹出并压入S,或将S的顶元素弹出并压入B,经一系列以上操作后,A中元素全部转入B中,B=[ak1,…,akn>(右端为栈顶),则成为A的一个栈混洗。
S.push(A.pop())
B.push(S.pop())
同一输入序列,可有多种栈混洗,长度为n的序列,可能的混洗总数SP(n)<=n! ∑SP(k-1)×SP(n-k)=(2n)!/[(n+1)!n!]
Q:判断某一输入序列的任一排列是否为栈混洗?
A:观察任意三个元素能否按照相对次序出现在混洗中,与其它元素无关,如1≤i<j<k≤n,[…,k,…,i,…,j,…>必非栈混洗,即对于输入序列<1,2,3],不存在312模式的序列。
O(n)算法:直接借助栈A、B、S,模拟混洗过程,每次S.pop()之前检测S是否已空;或需弹出的元素在S中,却非顶元素。每一次栈混洗,都对应于栈S的n次push与n次pop操作构成的序列。
(c4)栈应用:中缀表达式求值
给定语法正确的算术表达式S,计算与之对应的数值。
求值算法=栈+线性扫描
实现:主算法
float evaluate( char* S, char* & RPN){//中缀表达式求值
Stack<float> opnd; Stack<char> optr; //运算数栈、运算符栈
optr.push('\0'); //尾哨兵‘\0’也作为头哨兵首先入栈
while(!optr.empty()){//逐个处理各字符,直至运算符栈为空
if(isdigit(*S)) //若当前字符为操作数,则
readNumber(S,opnd); //读入(可能多位)操作数
else //若当前字符为运算符,则视其与栈顶运算符之间优先级高低
switch(orderBetween(optr.top(),*S)){/*分别处理*/}
}//while
return opnd.pop();//弹出并返回最后的计算结果
}
实现:优先级表
const char pri[N_OPTR][N_OPTR]={//运算符优先等级[栈顶][当前]//};
实现:不同优先级处理方法
switch(orderBetween(optr.top(),*S)){
case '<': //栈顶运算符优先级更低
optr.push(*S);S++;break; //计算推迟,当前运算符进栈
case '=': //优先级相等(当前运算符为右括号,或尾部哨兵'\0')
optr.pop();S++;break; //脱括号并接受下一个字符
case '>':{//栈顶运算符优先级更高,实施相应的计算,结果入栈
char op = optr.pop();
if('!' == op) opnd.push(calcu(op,opnd.pop()));//一元运算符
else{float pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop()};//二元运算符
opnd.push(calcu(pOpnd1,op,pOpnd2)); //实施计算,结果入栈
}
break;
}
(c5)栈应用:逆波兰表达式
RPN:逆波兰表达式。在由运算符和操作数组成的表达式中,不使用括号,即可表示带优先级的运算关系。
infix到postfix:手工转换:(1)用括号显式地表示优先级;(2)将运算符移到对应的右括号后;(3)抹去所有括号;(4)稍事整理。
infix到postfix:转换算法
float evaluate(char* S, char* & RPN){//RPN转换
/*.........*/
while(!optr.empty()){//逐个处理各字符,直至运算符栈空
if (isdigit(*S)) //若当前字符为操作数,将其直接接入RPN
{readNumber(S,opnd)};append(RPN,opnd.top();)
else //若当前字符为运算符
switch(orderBetween(optr.top,*S))
/*.............................*/
case '>' {//可立刻执行,在执行相应计算同时将其接入RPN
char op = optr.pop();append(RPN,op);//接入RPN
/*.........................*/
}
/*................................*/
}
}
(d)队列接口与实现
队列也是受限的序列
只能在队尾插入(查询):enqueue() + rear()
只能在队头删除(查询):dequeue() + front()
操作实例
Queue()
empty()
enqueue(3)
dequeue() //返回队首元素
front() //返回队首元素
size()
模板类:属于序列的特列,可基于向量或列表派生
template<typename T> class Queue: public List<T>{//由列表派生
public://size()与empty()直接沿用
void enqueue(T const & e){insertAsLast(e);} //入队
T dequeue(){return remove(first());}//出队
T & front() {return first()->data;}//队首
}//以列表首/末尾队列头/尾
均只需O(1) 时间