代码改变世界

前缀,中缀,后缀表达式学习笔记(2)

2012-05-03 15:51 MichaelYin 阅读(...) 评论(...) 编辑 收藏

前面一章已经将前缀,中缀,后缀表达式的基本概念进行了介绍,同时也介绍了编程中较常用的中缀表达式转换到后缀和前缀表达式的方法。接下来主要针对前缀,中缀和后缀表达式求值来进行相应的讨论。

在上一篇BLOG中的开始就阐述了中缀表达式求值的过程,中缀表达式求值比较符合人脑的计算方式,而对于计算机来说并不是十分高效,需要多次对表达式进行遍历。下面贴出我自己实现的直接对中缀表达式按人脑计算方式进行求值的代码。

public static int Evaluate(char[] exp, int firstIndex, int lastIndex)
{
    //外层如果是括号则去掉
    if (exp[firstIndex] == '(' && exp[lastIndex] == ')')
    {
        return Evaluate(exp, firstIndex + 1, lastIndex - 1);
    }
    //进行计算
    int position = 0;
    char result = AddOrSub(exp, firstIndex, lastIndex, ref position);
    if (result != '#')
    {
        //计算
        if (result == '+')
        {
            return Evaluate(exp, firstIndex, position - 1) + Evaluate(exp, position + 1, lastIndex);
        }
        if (result == '-')
        {
            return Evaluate(exp, firstIndex, position - 1) - Evaluate(exp, position + 1, lastIndex);
        }
    }
 
    result = MultiplyOrDevide(exp, firstIndex, lastIndex, ref position);
    if (result != '#')
    {
        //计算
        if (result == '*')
        {
            return Evaluate(exp, firstIndex, position - 1) * Evaluate(exp, position + 1, lastIndex);
        }
        if (result == '/')
        {
            return Evaluate(exp, firstIndex, position - 1) / Evaluate(exp, position + 1, lastIndex);
        }
    }
    //生成实际数字值 
    if (exp[firstIndex] > '9' || exp[firstIndex] < '0')
        Console.WriteLine("Error");
 
    return RealNum(exp[firstIndex]);
}
 
public static char AddOrSub(char[] exp, int firstIndex, int lastIndex, ref int position)
{
    System.Collections.Stack objStack = new Stack();
    //找到返回操作符,找不到返回#
    for (int i = firstIndex; i <= lastIndex; i++)
    {
        if (exp[i] == '(')
        {
            objStack.Push('(');
        }
        if (exp[i] == ')')
        {
            objStack.Pop();
        }
        if (exp[i] == '+' && objStack.Count == 0)
        {
            position = i;
            return '+';
        }
        if (exp[i] == '-' && objStack.Count == 0)
        {
            position = i;
            return '-';
        }
    }
    //没有找到对应的操作符
    return '#';
}
 
public static char MultiplyOrDevide(char[] exp, int firstIndex, int lastIndex, ref int position)
{
    //找到返回操作符,找不到返回#
    System.Collections.Stack objStack = new Stack();
    //找到返回操作符,找不到返回#
    for (int i = firstIndex; i <= lastIndex; i++)
    {
        if (exp[i] == '(')
        {
            objStack.Push('(');
        }
        if (exp[i] == ')')
        {
            objStack.Pop();
        }
        if (exp[i] == '*' && objStack.Count == 0)
        {
            position = i;
            return '*';
        }
        if (exp[i] == '/' && objStack.Count == 0)
        {
            position = i;
            return '/';
        }
    }
    //没有找到对应的操作符
    return '#';
}
 
public static int RealNum(char num)
{
    return 0 + (num - '0');
}

下面讲解如何对后缀表达式进行求值。建立一个栈,从左至右遍历表达式,如果遇到操作数则压入栈,如果遇到操作符则弹出相应的操作数进行计算,并将计算的结果重新入栈,最后的结果就在栈顶。前缀表达式和后缀类似,在这里就不赘述了。

可以看出后缀表达式的求值也只需要对后缀表达式进行一次扫描,而前面我们已经知道中缀表达式转换到后缀表达式也是只需要扫描一次的。相对于上面的类似于人脑的计算方式,我们在这里可以考虑将中缀表达式转换为后缀表达式后再对其求值,这样来完成中缀表达式的求值。其实这种方式就是那本经典的清华严蔚敏的《数据结构》一书中对于四则运算的解决的思路。

下面贴出用C#实现的中缀转后缀并求值的代码,该代码可以用来进行四则运算

public static void EvaluateExpression()
  {
      System.Collections.Stack stackA = new System.Collections.Stack();
      System.Collections.Stack stackB = new System.Collections.Stack();
      stackA.Push('#');
      char user;
      user = (char)Console.Read();
      while (user != '#' || ((char)stackA.Peek() != '#'))
      {
          //操作符号 
          if (In(user))
          {
              //判断优先级
              switch (Precede((char)stackA.Peek(), user))
              {
                  case '>':
                      //顺序
                      char b = (char)stackB.Pop();
                      char a = (char)stackB.Pop();
                      char op = (char)stackA.Pop();
 
                      stackB.Push(Operate(a, b, op));
 
                      break;
                  case '=':
                      stackA.Pop();
                      user = (char)Console.Read();
                      break;
                  case '<':
                      stackA.Push(user);
                      user = (char)Console.Read();
                      break;
              }
          }
          else
          {
              if (user < '0' || user > '9')
              {
                  Console.WriteLine("Error");
                  return;
              }
              else
              {
                  stackB.Push(user);
                  user = (char)Console.Read();
              }
          }
 
      }
      Console.WriteLine((char)stackB.Pop());
      Console.ReadLine();
  }
 
  /// <summary>
  /// 如果是操作符号,则返回true
  /// </summary>
  /// <param name="c"></param>
  /// <returns></returns>
  public static bool In(char c)
  {
      //该写法值得商榷
      switch (c)
      {
          case '(':
              return true;
          case ')':
              return true;
          case '+':
              return true;
          case '-':
              return true;
          case '*':
              return true;
          case '/':
              return true;
          case '#':
              return true;
      }
      return false;
  }
 
  public static char Precede(char t1, char t2)
  {
      char f = '=';
      switch (t2)
      {
          case '+':
          case '-': if (t1 == '(' || t1 == '#')
                  f = '<';
              else
                  f = '>';
              break;
          case '*':
          case '/': if (t1 == '*' || t1 == '/' || t1 == ')')
                  f = '>';
              else
                  f = '<';
              break;
          case '(': if (t1 == ')')
              {
                  Console.WriteLine("error");
              }
              else
                  f = '<';
              break;
          case ')':
              switch (t1)
              {
                  case '(': f = '=';
                      break;
                  case '#':
                      break;
                  default: f = '>';
                      break;
              }
              break;
          case '#': switch (t1)
              {
                  case '#': f = '=';
                      break;
                  case '(':
                      break;
                  default: f = '>';
                      break;
              }
              break;
      }
      return f;
  }
 
  public static char Operate(char a, char b, char operate)
  {
      a = Convert.ToChar((int)a - 48);
      b = Convert.ToChar((int)b - 48);
      int c = 0;
      switch (operate)
      {
          case '+': c = a + b + 48;
              break;
          case '-': c = a - b + 48;
              break;
          case '*': c = a * b + 48;
              break;
          case '/': c = a / b + 48;
              break;
      }
      return Convert.ToChar(c);
  }
  #endregion

在研究一下两种针对中缀表达式求值的方法后我们可以通过思考得到一些结论,首先人脑的计算方式在计算机中并不是最优化的方法,除了递归调用带来的性能损失,对表达式多次遍历也是会带来性能损失。而先转换到后缀表达式后在对其进行求值则避免了上述问题,提高了整体的计算的性能。

由于中缀表达式在我们看来更为习惯,但是后缀表达式在计算中的优势,所以编译程序常把程序源代码中的中缀表达式转换为后缀表达式再进行运算。当时看到这里的时候也想过为什么编译为什么不采用前缀表达式,后来想了一下前一篇BLOG讲到过前缀的扫描方向是从右至左,所以在程序中处理的话可能相对于后缀表达式不是很方便,所以后缀是最合适的选择。