(六)栈的规则及应用
目标
1) 描述ADT栈的操作
2) 使用栈来判定代数表达式中分隔符是否正确配对
3) 使用栈将中缀表达式转变为后缀表达式
4) 使用栈计算后缀表达式的值
5) 使用栈计算中缀表达式的值
6) 使用程序中的栈
7) 描述Java运行时环境如何使用栈来跟踪方法的执行过程
5.1 ADT栈的规格说明
栈顶(top),栈顶项(top entry),入栈(push),出栈(pop),查看栈顶项(peek),一般地,不能在栈中查找某个具体的项。
抽象数据类型:栈 |
+push(newEntry : T) : void +pop() : T +peek() : T +isEmpty() : boolean +clear() : void |
设计决策:当栈空时pop和peek应该做什么?
- 假定ADT不空。即增加一个能保证这个假设的前置条件(pop,peek是public,不能信任客户遵从这些方法所需的任何前置条件)
- 返回null(具有二义性:ADT空 or 元素是null,需要客户调用第二个方法来解释另一个方法的动作)
- 抛出一个异常(此情况下,认为返回null是有效数据)
设计决策:当栈为空时,pop和peek应该抛出哪类异常:受检异常还是运行时异常?
如果方法的客户能在执行时从异常中合理地恢复,它就应该抛出受检异常。这种情况下,客户可以直接处理异常,或者将它传播到另一个方法中。如果将异常看作对你方法的不正常使用(即使用你方法的程序员的错误),则方法应该抛出运行时异常。运行时异常不需要(但可以)在throws子句中说明,而且也不需要(但可以)被客户捕获。
这里将栈为空时调用pop和peek方法看作客户的错误,所以抛出运行时异常。
安全说明:信任
你能信任一段代码吗?不能,除非能证明它的动作是正确且安全的,这种情形下它成为可信代码(trusted code)。能信任客户以确定的方式使用你的软件,所以遵从任何及所有的前置条件,并能正确解释返回码吗?不能。但是类内的私有方法确实能保证或信任,其前置条件要被遵从,它的返回值可被正确对待。
安全说明:设计原则
- 使用前置条件和后置条件来记录假设
- 不要信任客户能正确使用共有方法
- 避免返回值的二义性
- 宁愿抛出异常,也不要用返回值来表示一个问题
5.2 使用栈来处理代数表达式
将二元运算符放在其操作数中间,如a+b,为中缀表达式;放在操作数前,如+ab,为前缀表达式(有时称波兰表示法(Polish notation),由波兰数学家Jan Lukasiewicz于19世纪20年代提出);放在操作数后,如ab+,为后缀表达式(有时称逆波兰表示法(reverse Polish notation))。
5.2.1 问题求解:检查中缀表达式中平衡的分隔符(括号配对)
平衡表达式(balanced expression)包含配对正确的或平衡的(balanced)分隔符。
public class BalanceChecker { public static void main(String[] args) { String expression = "a {b [c (d + e)/2 - f] + 1}"; boolean isBalanced = BalanceChecker.checkBalance(expression); if (isBalanced) { System.out.println(expression + " is balanced"); } else { System.out.println(expression + " is not balanced"); } // end if } // end main /** * Decides whether the parentheses, brackets, and braces * in a string occur in left/right pairs. * @param expression: A string to be checked. * @return: True if the delimiters are paired correctly. */ public static boolean checkBalance(String expression) { StackInterface<Character> openDelimiterStack = new OurStsck<>(); int characterCount = expression.length(); boolean isBalanced = true; int index = 0; char nextCharacter = ' '; while (isBalanced && (index < characterCount)) { nextCharacter = expression.charAt(index); switch (nextCharacter){ case '{': case '[': case '(': openDelimiterStack.push(nextCharacter); break; case '}': case ']': case ')': if (openDelimiterStack.isEmpty()) { isBalanced = false; } else { char openCharacter = openDelimiterStack.pop(); isBalanced = isPaired(openCharacter, nextCharacter); } // end if break; default: break; // Ignore unexpected characters } // end switch index++; } // end while if (!openDelimiterStack.isEmpty()) { isBalanced = false; } // end if return isBalanced; } // end checkBalance // Returns true if the given characters, open and close, form a pair // of parentheses, brackets, or braces. private static boolean isPaired(char open, char close) { return (open == '(' && close == ')') || (open == '[' && close == ']') || (open == '{' && close == '}'); } // end isPaired } // end BalanceChecker
5.2.2 问题求解:将中缀代数表达式转换为后缀表达式
最终目标是如何计算中缀表达式,但后缀表达式更容易求职值,所以先看一个中缀表达式如何表示为后缀表达式的形式。 |
中缀表达式对应后缀表达式的例子:
中缀 |
后缀 |
a + b |
a b + |
(a + b) * c |
a b + c * |
a + b * c |
a b c * + |
1) 手算策略
将中缀表达式根据优先级添加括号,再转为后缀表达式。
2) 转换算法的基础
从左到右扫描中缀表达式,到遇到操作数时,将它放到正创建的新表达式的末尾。在中缀表达式中,操作数的顺序和其对应的后缀表达式中是一样的。当遇到运算符时,必须先保存它,直到能判定它在所属的输出表达式的位置为止。将运算符保存到栈中。一般地,至少要等到将它与下一个运算符的优先级进行比较时。如转换a+b*c,将+保存到栈中,当遇到b,不能将+拿出来,要看下一个运算符,下一个运算符为*,优先级高于+,所以保存*,输入c,拿*,再拿+,abc*+。
3) 具有相同优先级的连续运算符
如果两个相连的运算符有相同的优先级,需要区分满足从左至右结合律的运算符(即+、-、*、/)及求幂,后者满足从右到左结合律。如a-b+c,遇到+时,栈中含有-,且部分后缀表达式是ab,减号运算符属于操作数a和b,所以-出栈,有ab-,+进栈,最后出栈,有ab-c+。表达式a^b^c:遇到第二个求幂运算符时,栈中含有^,目前有ab,当前运算符与栈顶有相同的优先级,但因为a^b^c的含义是a^(b^c),所以将第二个^入栈,abc^^。
4) 圆括号
圆括号改变了运算符优先级规则,将开圆括号入栈,一旦它出现在栈中,将开圆括号看作有最低优先级的运算符。即,后面的任何运算符都将入栈。当遇到闭圆括号时,将运算符出栈,且加到已有的后缀表达式的后面,直到弹出一个开圆括号时为止。算法继续,但不将圆括号加到后缀表达式。
5) 中缀—后缀的转换
Algorithm convertToPostfix(infix) // 将中缀表达式转换为等价的后缀表达式
operatorStack = 一个新的空栈 postfix = 一个新的空字符串 while (infix还有待解析的字符){ nextCharacter = infix的下一个非空字符 switch (nextCharacter){ case 变量: 将nextCharacter添加到postfix的后面 break case '^': operatorStack.push(nextCharacter) break case '+': case '-': case '*': case '/': while (!operatorStack.isEmpty() && nextCharater的优先级 <= operatorStack.peek()的优先级){ 将operatorStack.peek()添加到postfix的后面 operatorStack.pop() } operatorStack.push(nextCharacter) break case '(': operatorStack.push(nextCharater) break case ')': // 如果中缀表达式合法,则栈非空 topOperator = operatorStack.pop() while (topOperator != '('){ 将topOperator添加到postfix的后面 topOperator = operatorStack.pop() } break default: break; // 忽略预期之外的字符 } } while (!operatorStack.isEmpty()){ topOperator = operatorStack.pop() 将topOperator添加到postfix的后面 } return postfix
5.2.3 问题求解:计算后缀表达式的值
遍历后缀表达式,遇见值入栈,遇见操作符,出栈两次,计算后结果入栈,直到栈中只有一个数值即表达式结果。
Algorithm evaluatePostfix(postfix) // 计算后缀表达式 valueStack = 一个新的空栈 while (postfix还有待解析的字符){ nextCharacter = postfix的下一个非空字符 switch (nextCharacter){ case 变量: valueStack.push(变量nextCharacter的值) break case '+': case '-': case '*': case '/': operandTwo = valueStack.pop() operandOne = valueStack.pop() result = nextCharacter中的操作作用于其操作数operandOne和operandTwo valueStack.push(result) break default: break // 忽略预期之外的字符 } } return valueStack.pop()
5.2.4 问题求解:计算中缀表达式的值
将前两个算法合为一个算法,使用两个栈直接计算中缀表达式的值。合并算法根据中缀表达式转换为后缀形式的算法,维护一个运算符栈。但该算法不将操作数添加到表达式的末尾,而是根据计算后缀表达式的算法,将操作数的值压入第二个栈中。
Algorithm evaluateInfix(infix) // 计算中缀表达式 operatorStack = 一个新的空栈 valueStack = 一个新的空栈 while (infix还有待解析的字符){ nextCharacter = infix的下一个非空字符 switch (nextCharacter){ case 变量: valueStack.push(变量nextCharacter的值) break case '^': operatorStack.push(nextCharacter) break case '+': case '-': case '*': case '/': while (!operatorStack.isEmpty() && nextCharater的优先级 <= operatorStack.peek()的优先级){ // 执行operatorStack栈顶的操作 topOperator = operatorStack.pop() operandTwo = valueStack.pop() operandOne = valueStack.pop() result = nextCharacter中的操作作用于其操作数operandOne和operandTwo valueStack.push(result) } operatorStack.push(nextCharacter) break case '(': operatorStack.push(nextCharater) break case ')': // 如果中缀表达式合法,则栈非空 topOperator = operatorStack.pop() while (topOperator != '('){ operandTwo = valueStack.pop() operandOne = valueStack.pop() result = nextCharacter中的操作作用于其操作数operandOne和operandTwo valueStack.push(result) topOperator = operatorStack.pop() } break default: break; // 忽略预期之外的字符 } } while (!operatorStack.isEmpty()){ topOperator = operatorStack.pop() operandTwo = valueStack.pop() operandOne = valueStack.pop() result = nextCharacter中的操作作用于其操作数operandOne和operandTwo valueStack.push(result) } return valueStack.peek()
5.3 Java类库:类Stack
java类库含有类Stack,它实现了java.util包中的ADT栈。与我们定义的方法的不同之处已做标记。
public T push(T item); public T pop(); public T peek(); public boolean empty(); |
5.4 小结
- ADT栈按后进先出的原则组织项。栈顶的项是最新添加进来的
- 栈的主要操作(push, pop和peek)都仅处理栈顶。方法push将项添加到栈顶;pop删除并返回栈顶,而peak只是返回栈顶
- 普通的代数表达式称为中缀表达式,因为每个二元运算符出现在它的两个操作数的中间。中缀表达式需要运算符优先级规则,且可使用圆括号来改变这些规则。
- 可以使用值栈来计算后缀表达式的值
- 可以使用两个栈(一个用于运算符,一个用于值)来计算中缀表达式的值
- 像peek和pop这样的方法,当栈为空时必须有合理的动作。例如,它们可以返回null或者抛出一个异常
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术