目录
栈
LIFO结构,函数递归调用的底层实现
括号匹配问题
栈的简单应用,过程中与结束时检查是否为空栈。
考虑文法
括号匹配问题为其LR(0)文法分析过程。
若引入多种括号,左括号进栈,右括号与栈顶匹配则栈顶出栈,继续识别;与栈顶不匹配(在当前左括号还未匹配时识别到其他类别的括号)即得匹配失败。
栈混洗问题
有三个栈A、B、C,A中有元素,B、C皆为空。只考虑下面两种操作:
- A出栈,B压栈
- B出栈,C压栈
把A、B中元素全部出栈,最终在C中得到的元素序列称为原序列的一个栈混洗。
栈混洗禁形:不失一般性,设A中元素为,为一个排列,易见上述排列是一个栈混洗当且仅当不存在在上述排列中。
视操作1为括号匹配问题中识别到左括号的动作,操作2为括号匹配问题中识别到右括号的动作,那么
- 一个括号匹配的序列意味着在任何位置操作1次数不少于操作2次数,而总体操作1次数与操作2次数相同,这给出了一个栈混洗序列
- 一个栈混洗序列的产生意味着在任何位置操作1次数不少于操作2次数,而总体操作1次数与操作2次数相同,对应了一个括号匹配序列
这表明A的栈混洗序列与n对括号的匹配序列构成双射,A的栈混洗数量与n对括号的匹配序列数量相同。
n对括号的匹配序列数量为,其中为第n个Catalan数。
对于任何n对括号匹配的表达式Sn,有拆解Sn=Sk(Sn-k-1),0<=k<n,从而记Sn有Pn种可能,有递推关系和边界条件,对照Catalan数递推关系即得。
中缀表达式求值与中缀表达式转换为RPN
按运算符优先级计算中缀表达式。计算过程中按计算顺序追加RPN。
通常计算规则下的语法制导翻译
#define N_OPTR 9 //运算符总数
typedef enum { ADD, SUB, MUL, DIV, POW, FAC, L_P, R_P, EOE } Operator; //运算符集合
//加、减、乘、除、乘方、阶乘、左括号、右括号、起始符与终止符
const char pri[N_OPTR][N_OPTR] = { //运算符优先等级 [栈顶] [当前]
/* |-------------------- 当 前 运 算 符 --------------------| */
/* + - * / ^ ! ( ) \0 */
/* -- + */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
/* | - */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
/* 栈 * */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
/* 顶 / */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
/* 运 ^ */ '>', '>', '>', '>', '>', '<', '<', '>', '>',
/* 算 ! */ '>', '>', '>', '>', '>', '>', ' ', '>', '>',
/* 符 ( */ '<', '<', '<', '<', '<', '<', '<', '=', ' ', //当栈顶运算符是左括号但已经扫描到终结符时,括号不匹配
/* | ) */ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', //当栈顶运算符是右括号时,括号不匹配
/* -- \0 */ '<', '<', '<', '<', '<', '<', '<', ' ', '='
};
float evaluate (char* S, char*& RPN) //对(已剔除空格符)表达式S求值,并转换为逆波兰式RPN
{
Stack<float> opnd; Stack<char> optr; //运算数栈、运算符栈
optr.push ( '\0' ); //尾哨兵'\0'也作为头哨兵首先入栈
while (!optr.empty())
{ //在运算符栈非空前,逐个处理表达式中各字符
if ( isdigit ( *S ) ) { //若当前字符为操作数,则
readNumber ( S, opnd ); append ( RPN, opnd.top() ); //读入操作数,并将其接至RPN末尾
}
else //若当前字符为运算符,则
{
switch ( orderBetween ( optr.top(), *S ) )
{
case '<': //栈顶运算符优先级更低时
{
optr.push ( *S ); S++; //计算推迟,当前运算符进栈
break;
}
case '=': //优先级相等(当前运算符为右括号或者尾部哨兵'\0')时
{
optr.pop(); S++; //脱括号并接收下一个字符
break;
}
case '>': //栈顶运算符优先级更高时,可实施相应的计算,并将结果重新入栈
{
char op = optr.pop(); append ( RPN, op ); //栈顶运算符出栈幵续接至RPN末尾
if ('!' == op) { //若属于一元运算符
float pOpnd = opnd.pop(); //取出一个操作数
opnd.push ( calcu ( op, pOpnd ) ); //一元计算,结果入栈
}
else //对于其它(二元)运算符
{
float pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop(); //取出后、前操作数
opnd.push ( calcu ( pOpnd1, op, pOpnd2 ) ); //二元计算,结果入栈
}
break;
}
default : exit ( -1 ); //语法错误
}}//switch
}//while
return opnd.pop(); //弹出并返回最后的计算结果
}
开头增加开始符哨兵,优先级低于常规计算符和左括号。栈顶为开始符时识别到常规计算符和左括号,操作符进栈。
相同等级的运算符表项均记为>,出现此种情况即栈顶出栈计算。
evaluate()函数对语法错误的表达式可能亦能给出结果,例如(12)3+!4*+5。
(2018-912)非法表达式(12)3+!4*+5执行evaluate算法后的结果为______。
输入缓冲 操作数栈 操作符栈 动作 (12)3+!4*+5$ $ 识别到开始符 12)3+!4*+5$ $ ( 识别到(,栈顶为$,栈顶<当前,(入栈 )3+!4*+5$ 12 $ 12入栈(readNumber函数识别最长操作数字符串) 3+!4*+5$ 12 $ 识别到),栈顶为(,脱括号,推进一个字符 +!4*+5$ 12 3 $ 3入栈 !4*+5$ 12 3 $ + +入栈 4*+5$ 12 3 $ + ! 识别到!,栈顶为+,栈顶<当前,!入栈 *+5$ 12 3 4 $ + ! 4入栈 *+5$ 12 3 24 $ + 识别到*,栈顶为!,栈顶>当前,执行! +5$ 12 3 24 $ + * 识别到*,栈顶为+,栈顶<当前,*入栈 +5$ 12 72 $ + 识别到+,栈顶为*,栈顶>当前,执行* +5$ 84 $ 识别到+,栈顶为+,栈顶>当前,执行+ 5$ 84 $ + +入栈 $ 84 5 $ + 5入栈 $ 89 $ 识别到$,栈顶为+,栈顶>当前,执行+ $ 89 识别到$,栈顶为$,栈顶=当前,$出栈 $ 89 操作符栈为空,while循环退出,得89
RPN求值从左向右执行即可
infix为语法解析树的中序遍历,postfix为语法解析树的后序遍历
- 中缀表达式需要加括号以规定计算顺序,但无需元字符分隔相邻操作数
- RPN不需要加括号,但需要元字符分隔相邻操作数(11 2 +和1 12+)
利用栈判断回文串
找到中间位置即可
一向量多栈
设置每个栈的sp,类比内存栈管理
倒置栈
利用递归机制存数据,时间复杂度O(n^2),空间复杂度为递归深度O(n)
直方图中最大矩形问题
按照从左到右的顺序处理元素,并且维护一个包含已经开始但尚未结束的子直方图信息的栈。
不妨认为相邻直方高度不同。
- 如果当前高度大于栈顶元素,这表明栈顶元素高度的直方可以延伸,入栈
- 如果当前高度小于栈顶元素,这表明栈顶元素高度的直方到此结束了
- 计算此时面积
- 矩形的高是栈顶元素
- 矩形的底是index(当前)-index(栈顶)
- 更新最大值并出栈,继续检查
- 计算此时面积
- 结束时可增加哨兵,高度为0
分摊时间复杂度为O(n),空间复杂度为O(n)(维护的栈)。
队列
FIFO结构,出队端为队头,进队端为队尾
双栈当队
一个进一个出,用完一个就把另一个pop-push
双队当栈
确保至少一个队列始终为空
- push:哪个不空进哪个
- pop:把前面所有的移入另一空队列中,最后一个出队(现在这个队列是空的了)
参考:
1. 邓俊辉. 数据结构(C++语言版)
2. Narasimha Karumanchi. Data Structures and Algorithms Made Easy: Data Structures and Algorithmic Puzzles.