四则运算(挑战出题)解答之轮子哥版-2
上一次解读 轮子哥版本四则运算(挑战出题)的博客在 这里。但是,上次博客中只是针对交换律做解读,而轮子哥还考虑了结合律,在归一化部分体现。
尽可能左结合
在生成表达式时尽可能左结合。什么意思呢?
比如:
1+2+3
是左结合,其后缀表达式:12+3+
1+(2+3)
不是左结合,其后缀表达式:123++
,我们要避免生成这样的表达式
为什么要这么做?
以上两个算式考虑结合律的情况下是相同的算式,但是后缀表达式不同,而我们目的是要简单通过后缀表达式是否相同来判断算式是否相同,所以生成的时候按规则左结合来避免这种情况。
好处?
对于string的比较和去重已经非常成熟,不需要我们重复造轮子,C++中 set<string>
就能搞定。
如果是构造表达式树(树结构)去重,要么需要对树进行翻转递归比较(效率低),要么需要自己对表达式树做可靠的hash(又是造轮子)后比较hash值。
怎么做?
我们是直接生成后缀表达式,从上面可以观察到连续生成+
或者*
不是我们想要的,所以在生成的过程中需要避免。
根据上一篇博客介绍,我们在生成过程中维护了表达式树的结构,我们在随机生成新的字符时,只需要观察右子树最后一个字符是什么(其实就是整个表达式最后一个字符),如果是+
或者*
,我们就避免重复生成。
归一化
- 使得相同表达式的后缀表达式是一样
- 交换律/结合律同时考虑(只有
+
和*
运算符需要) - 边生成边进行,而不是生成完了再来做
引用一段代码注释:
针对交换律和结合律进行归一化。 也就是说,无论是(1+2)*(3+4)还是(4+3)*(2+1)都将返回12+34+*。 因为"12+"<"34+",所以顺序将会被调整,把小的放在左边。 因此在产生 (4+3)*(2+1) 的过程中 完成43+就立即调整为34+ 完成21+就立即调整为12+ 完成34+12+*就立即调整为12+34+*
即在生成运算符(+
或*
)的时候来做该运算符所属的两棵子表达式的顺序调整(字符串排序),当前运算符的位置不变。
回到刚才的尽量左结合问题,在上述归一化过程中,可能会产生不是尽可能左结合的情况,比如:在产生+
的情况下,当前表达式是23+1
(两边的表达式分别是23+
和1
),直接调整后就是123++
,不是左结合。
这种情况如何处理呢?
举例:
-
如果这次生成
+
,在向前分割表达式树时,如果还遇到运算符是+
的表达式树,就继续分解下去,继续描述上述情况,即23+
的运算符是+
(与本次生成的运算符相同),应该继续被分解成2
,3
两个子表达式,然后对各个子表达式进行排序,运算符位置不变,即得到12+3+
。- 这里的
1
,2
,3
可以代表复杂的子表达式。
- 这里的
-
对于生成
*
的情况是相同的。
这部分代码位于:https://gist.github.com/vczh/2c058aed996effc0a519ed3d265a3eb5#file-xinz_expr_gen-cpp-L196
看几个实际的例子
简单起始情况
- 假设已经产生的表达式:
32
- 当前生成
*
->32*
(中缀表达式为3 * 2
) - 乘法可交换,因此要拆分其左右表达式树,做排序
- 往前拆分子表达式得到
3
,2
- 排序
"2" < "3"
, 得到23
- 做拆分的运算符位置都保持不变,得到
23*
(中缀表达式为2 * 3
)
- 往前拆分子表达式得到
当前生成 +
时同理,即无论生成:
23+
32+
最后都会是 23+
稍复杂点
- 假设已经产生的表达式:
85-4/712+-
- 当前生成
*
->85-4/712+-*
(中缀表达式为(8-5)/4*(7-(1+2))
) - 乘法可交换,因此要拆分其左右表达式树,做排序
- 往前拆分子表达式得到
85-4/
,712+-
- 排序
"712+-" < "85-4/"
, 得到712+-85-4/
- 做拆分的运算符位置都保持不变,得到
712+-85-4/*
(中缀表达式为(7-(1+2)) * ((8-5)/4)
)
- 往前拆分子表达式得到
交换导致违反尽可能左结合的简单情况
这种情况在上面已经说过,还是简单罗列一下:
- 假设已经产生的表达式:
23+1
- 当前生成
+
->23+1+
(中缀表达式为2+3+1
) - 加法可交换,因此要拆分其左右表达式树,做排序
- 往前拆分子表达式得到
23+
,1
- 排序
"1" < "23+"
, 得到123+
- 做拆分的运算符位置都保持不变,得到
123++
(中缀表达式为1+(2+3)
)
- 往前拆分子表达式得到
显然这不是我们所希望发生的,因为这将阻碍我们想简单通过后缀表达式来判断重复。怎么处理这种情况,上面也已经说了,这里也简单列出:
- 加法可交换,因此要拆分其左右表达式树,做排序
- 往前拆分子表达式得到
23+
,1
,23+
是+
运算,与当前产生的运算符相同,所以要继续对这个子表达式做拆分,得到以下子表达式:2
,3
,1
- 排序
"1" < "2" < "3"
, 得到123
- 做拆分的运算符位置都保持不变,得到
12+3+
(中缀表达式为1+2+3
)
- 往前拆分子表达式得到
同样,当前生成*
时,子表达式为 *
运算时,也要继续拆分。
交换导致违反尽可能左结合的稍复杂情况
其实将上一个例子中的1
,2
,3
替换成复杂的子表达式即可,需要注意的是,我们每一次生成+
或*
都要进行上述操作,所以在假设已经产生的表达式时,要符合上述规则调整后的情况。