(六)栈的规则及应用

目标

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或者抛出一个异常

                                                                        

posted @ 2018-09-12 22:46  dedication  阅读(904)  评论(0编辑  收藏  举报