`
frank-liu
  • 浏览: 1682358 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

算术表达式括号补全问题分析

 
阅读更多

问题描述

    假定我们有一个数学表达式,我们把里面所有的左括号都去掉了,比如说:1 + 2 ) * 3 - 4 ) * 5 - 6 ) ) )。我们需要找到一个方法将缺失的这部分左括号补全使得它成为一个完整正确的表达式。对应于前面的示例,它对应的完整的数学表达式为:( ( 1 + 2 ) * ( ( 3 - 4 ) * ( 5 - 6 ) ) )。这里的有一个要求是所有表达式必须是用括号包含的,但是又不包含冗余的。

 

问题分析

    粗看这个问题的时候,有点不知道从哪里下手。因为对于一个表达式来说,比如前面的1 + 2 ) * 3 - 4 ) * 5 - 6 ) ) )。因为我们这里不考虑对单独的数字用一组括号包含起来。所以不存在如(1)这样的情况。 所以这里第一个关键的点就是,我们里面所有括号里包含的表达式必然是一个(a op b)这种样式。这里假设a和b是一个子表达式, 而op表示一个运算符。对于比如((1 + 2) * (3 + 4))这样的表达式来说,它也相当于我们前面的一种表达式。不过光从这一点来分析的话,我们似乎还是没有多少线索。看来还需要进一步的分析。

 

歧义分析

    最开始要找这些个匹配的括号时,最让人困惑的地方就是,感觉似乎有很多种可能。然后就没法确定了。比如说,在我们前面的表达式里,我们既可以组织表达式成( ( 1 + 2 ) * (3 - 4) ), 也可以组织成(1 + (2 * ( 3 - 4) ) ) 这种。这样,当我们读取到某个操作符后面的数字时,就不知道该将这个数字包含到哪个部分。这部分解读看似有点道理,但是我们还忽略了一个地方。就是对于我们要找到的符号匹配,我们虽然没有了左括号,可是我们是有右括号的。在前面的示例中,虽然我们组成的表达式不一样,但是它们右括号的数量不一样。也就是说,采用不一样数目的右括号,才能对应不同的表达式。至此,我们可以推断,右括号的布局和数量其实在某种程度上已经确定了这个表达式。

    我们现在再来看一个简单的示例:比如说1 + 2 * 3。 我们将他们组合成这种表达式:((1 + 2) * 3) 也可以组合成这种表达式:((1 + (2 * 3))。 在这里,我们组合的方式不一样,但是右括号的位置和数量也有差别。在前面这种情况下,我们的两个右括号一个是在2的右边,一个是在3的右边。而后面这种则是两个都在3的后边。可见,在这里我们至少可以确定右括号的位置和数量不同它们就确定了唯一不同的表达式结构。现在的问题是,就算我们知道它们不同,有什么办法把这个表达式的完整结构给整出来呢?

 

进一步分析

    我们知道对于一个表达式来说,因为我们这里采取用括号来包括他们的结构和运算关系,他们可以笼统的概括成一个(a op b)的这种样式。这样就形成了一个递归定义的结构。而如果我们对编译原理的一些概念还有点印象的话,会想到抽象语法树这个东西。比如说a + b 则可以形成一个以+为根而分别以a, b为左右子节点的二叉树。那么,以我们前面列举的表达式( ( 1 + 2 ) * ( ( 3 - 4 ) * ( 5 - 6 ) ) )为例,它对应的构造语法树会是个什么样子呢? 我们一步步的把它构建出来:

    首先,它既然是一个(a op b)这种结构。那么当我们把最外面那一层括号剥除的时候,它对应的应该是(1 + 2) * ((3- 4) * (5 - 6)) ,其中a对应(1 + 2), op操作符对应 *, b对应((3-4) * (5 - 6))。 它对应的语法树结构如下:

    我们可以进一步的展开(1 + 2),它既然也应该对应(a op b)这种结构,那么a= 1, b = 2, op = +:

    右边的子节点也按照这种方式展开:

    最后的结果将如下图:

 

     刚才这个过程展示了两点。一个就是我们每展开一个节点的时候,相当于去掉了一层括号。另外就是每个节点最终是一个数字或者两个数字和一个计算符号的组合。我们回顾一下刚才这个展开的过程。每次我们要去掉一组括号,对应的这个括号里就必然包含这一个计算表达式加上一个运算符再加上一个表达式。我们这样不断的去掉括号,不断的构造出来了这个表达式树。

    而如果我们把前面的这个过程倒过来呢?我们每次不断的合并一个表达式加上一个元算符再加上一个表达式,然后把他们加上括号,这里不就构造出我们原来的这个大的数学表达式了吗?没错,这一步是确定的。可是如果我们要这样做的话,这里是要求前面的左右括号都有。而我们这里是去掉了左括号。不过没关系,因为我们既然有了右括号,我们就知道,对于一个完整的表达式来说,它至少应该保证它的左右括号数量是一样的。所以对于一个右括号来说,当我们从左到右遍历,碰到第一个右括号时,它必然是构成一个最小表达式的一部分。而如果要构成一个最小的表达式来说,无非就是像1 + 2, 2 * 3等这样的形式。对于这种1 + 2 ),或者 2 * 3 ),我们很显然,只要将它前面的两个数字和符号组合起来,再在左边加上左括号就可以了。

     前面的这一步,相当于构造了一个表达式,它本身也将作为另外一个大的表达式的一部分。我们按照类似递归的概念。既然这个右括号它所涵盖的这部分,比如(1 + 2)是一个表达式,这个时候,如果我们将它作为一个整体,在后面再碰到右括号的时候,我们是不是同样也可以把它当作第一个右括号那样来使用呢?比如说,我们前面的表达式((1 + 2) * 3),在去掉左括号的时候它是1 + 2) * 3。按照刚才的思路,我们应该是组合( 1 + 2 ), 然后我们碰到运算符号* 和数字3。接着我们又碰到右括号,这个时候类似,我们将右括号前面的两个运算表达式和运算符组合起来,这就是((1 + 2) * 3)。我们再看((1 + (2 * 3)) 这个。它去掉左括号则是1 + 2 * 3))。我们在碰到第一个右括号的时候,组合的第一个表达式是(2 * 3),然后再碰到另外一个右括号,这个时候再组合,就有(1 + (2 * 3))。 

    哈哈,看来到了这一步,我们找到了一个构造表达式的规律了。无非就是每次我们碰到一个右括号的时候,将这个右括号左边的两个表达式和一个运算符组合成一个表达式。这样不断循环到最后。

 

实现 

    从前面的讨论里,我们就已经知道一个最终解析的方法了。就是通过碰到一个右括号,然后将它前面的部分组合起来构成新的表达式。这个新的表达式也将作为一个子节点参与到后面的组合中。那么,从实现的角度来说,我们可以发现如果用栈来解决这个问题简直就是最理想的方法。因为每次都要取前面的结果,而这些结果是对应每个右括号最接近的,这不就是对应着栈的LIFO吗?假设我们用栈来解析前面的表达式1 + 2 ) * 3 - 4 ) * 5 - 6 ) ) ),那么该是个什么样的过程呢?

    我们将这个过程的图示标注下来:

         首先,我们碰到1, +, 2, 那么这3个元素依次入栈。如下图:

    

 

    这个时候,我们碰到第一个右括号,我们需要将栈里的两个表达式和一个运算符取出来,然后组成(1 + 2),然后将这个组合的表达式入栈:

    然后我们将* 3 - 4这部分入栈:

     这个时候我们又碰到一个右括号,老规矩,弹出前面的两个表达式和一个运算符,则有(3 - 4),再将它入栈:

     接着是后面的* 5 - 6:

     然后我们后面有3个有括号,我们一个个的来处理:

     第二个右括号:

     最后一个:

 

    在上面的每次调整的过程中,我们将前面压在栈底的3个元素都弹出来,然后组织的时候是后面弹出的元素放在前面,然后再包装一层括号,再入栈。这就是我们每次弹栈后做的事情。这样最后栈顶剩下的那个元素就是我们拼装出来的结果。

    有了前面的这个描述,我们就可以很容易得到代码实现了:

 

public static String parse(String[] tokens) {        
        Stack<String> stack = new Stack<String>();
        for(String str : tokens) {
            System.out.println(str);
            if(str.equals(")")) {
                StringBuilder builder = new StringBuilder();
                builder.append("(");
                String op2 = stack.pop();
                String operator = stack.pop();
                String op1 = stack.pop();
                builder.append(op1);
                builder.append(operator);
                builder.append(op2);
                builder.append(str);
                stack.push(builder.toString());
            } else {
                stack.push(str);
            }
        }
        return stack.peek();
    }

     前面已经说的很清楚,这里无需解释。你懂的。

 

总结

    这里主要是针对一个算术表达式的结构在它的右括号已经确定的情况下,讨论怎么去填充它的左括号。我们会发现实际上对于一个表达式来说,只要有一边的括号是完全确定的,它就可以唯一的确定这个表达式了。同样的,假设我们有了左括号,对于缺失的右括号来说也可以按照同样的思路来补。对于这些问题后面的数学原理我们暂时没有深究。实际上它和数学上的catalan数有着密切的关系。在后续相关的文章里会做相应的分析。

 

参考材料

http://www.amazon.com/Algorithms-4th-Robert-Sedgewick/dp/032157351X/ref=sr_1_1?s=books&ie=UTF8&qid=1403251503&sr=1-1&keywords=algorithms

  • 大小: 6.4 KB
  • 大小: 7.5 KB
  • 大小: 8 KB
  • 大小: 11.4 KB
  • 大小: 2.6 KB
  • 大小: 2.3 KB
  • 大小: 5 KB
  • 大小: 4 KB
  • 大小: 6.8 KB
  • 大小: 6 KB
  • 大小: 6.5 KB
  • 大小: 6.1 KB
分享到:
评论

相关推荐

    方括号下载:下载方括号(正在下降)

    这个应用可能有一个特色功能与“括号”有关,例如代码高亮、自动补全,或者是在处理括号语法时有独特的优化。由于下载过程中的“下降”描述,这可能是一个实时显示下载状态的服务,让用户了解他们的下载速度变化。 ...

    初一数学上册期中考试试卷及答案【人教版新课标】精选.doc

    17. 计算题,可能包括基本的算术运算或简单的代数表达式的求解。 18. 解方程,可能有线性方程和简单的代数步骤。 19. 生产计划问题,可能需要计算每日实际生产的自行车数量与计划的差异。 这些题目综合考察了初一...

    Shells编程快速入门(四)之Bash Shell.pdf

    `$()`也可用于命令替换和算术表达式,如`echo $(( n + 5 ))`。 8. **操作符**: - **相等性操作符**:`=`, `!=` - **关系操作符**:`, `&gt;`, `, `&gt;=` - **逻辑操作符**:`&&`, `||`, `!` 9. **条件语句**: - `...

    README重要1

    - **逆波兰表示法简介**:是一种没有括号的数学表达式表示方法,可以简化表达式的求值过程。 - **实现细节**: - 通常通过栈数据结构来实现逆波兰表达式的转换和计算。 - 支持基本的算术运算符如加、减、乘、除等...

    2021年人教版一年级数学上册第一次月考考试附答案(20211025003756).pdf

    7. 逻辑推理和数学问题解决:内容中提到的“()5/66206/6”,可能是要求学生根据给出的数学信息进行推理,解出括号中的数字或数学表达式。 8. 数字大小比较:内容中有一些数字被排列在一起,如“10391C2D3B 4B 5B”...

    python学习导航.txt

    - 运算符与表达式:Python支持各种运算符,包括算术运算符(加、减、乘、除等)、比较运算符(大于、小于、等于等)、逻辑运算符(and、or、not)、位运算符(与、或、非、异或等)。表达式用于构造运算,Python采用...

    碥小幼儿园升小学数学测试题.pdf

    1. 基本的算术运算:文件中出现了一系列的数字和算术运算符,例如加号“+”,减号“-”,和乘号“x”。这表明测试题可能会涉及基础的四则运算。孩子们需要了解和掌握加法、减法以及乘法的概念和运算规则。 2. 等式...

    C语言学习指导讲义

    3. 熟悉算术运算符,如加法、减法、乘法、除法,以及自增`++`和自减`--`运算符的用法,了解它们在表达式中的作用。 4. 通过修改和运行程序,观察不同数据类型和赋值方式对程序结果的影响,例如字符型和整型的打印,...

    快乐的linux命令行

    - **算术表达式展开**:`bash`支持直接在命令行中计算简单的数学表达式,这对于动态生成数值非常有用。 - **花括号展开**:花括号(`{}`)可用于生成一系列字符串。这对于批量创建文件或目录非常有用。 - **参数展开...

    快乐的Linux命令行

    - **算术表达式展开**:介绍如何在Shell脚本中进行简单的数学运算。 - **花括号展开**:通过花括号来生成一系列字符串,便于批量操作。 - **参数展开**:讲解了如何使用变量名来访问和操作变量值。 - **命令替换**:...

    吉林省磐石市2012-2013学年七年级数学上学期期末教学检测试题(无答案) 新人教版

    这篇资料是一份针对七年级学生的数学期末教学检测试题,主要涵盖了新人教版的初中...这些题目覆盖了七年级数学的多个核心知识点,包括基本的算术、代数、几何以及应用问题,是检验学生理解和运用这些知识的重要工具。

    abs-guide_REvision_linux_shell_bash_

    8. **正则表达式和模式匹配**:结合grep、sed和awk等工具,掌握正则表达式的使用,用于文本查找、替换和处理。 9. **进程管理和作业控制**:学习如何在后台运行命令,使用jobs、fg、bg等命令进行作业控制,以及如何...

Global site tag (gtag.js) - Google Analytics