带二元运算符的数字集合表达式排列及去重算法问询
这确实是个挺有意思的表达式生成+去重问题,既要覆盖所有合法的带括号组合,又要干掉冗余括号和交换律带来的重复项,我来拆解下可行的思路和具体实现方案:
一、先搞定冗余括号:从根源避免生成,别等事后清理
冗余括号说白了就是那种不改变运算顺序的多余括号,比如(((2*3))+5)和(2*3)+5完全是一个意思,括号纯属多余。最省心的方式不是生成所有可能的括号再过滤,而是在生成表达式的时候就只加必要的括号。
具体怎么判断要不要加括号?咱们先给运算符定个优先级:比如*、/优先级设为2,+、-设为1。然后递归生成的时候,遵循这个规则:
- 如果当前外层运算符的优先级 高于 子表达式的根运算符,子表达式不用加括号;
- 如果外层运算符优先级和子表达式根运算符相同,要看结合性:比如
+、*是左结合,a+b+c等价于(a+b)+c,所以a+(b+c)里的括号其实是冗余的,咱们可以强制只生成左结合的形式,避免重复;而像-这种非交换左结合的,a-(b-c)和(a-b)-c完全不同,这时候b-c就必须加括号。
举个实际例子:
- 生成
2 * (3 + 5):因为+优先级比*低,所以3+5必须套括号; - 生成
2*3 +5:2*3的根运算符是*,优先级比+高,直接写2*3+5就行,根本不用搞((2*3)+5)这种花里胡哨的。
这样从生成阶段就把冗余括号掐死了,省了事后过滤的麻烦。
二、处理交换律重复:用「规范键」标记等价表达式
像a+b和b+a、(a+b)*c和c*(a+b)这种因为交换律产生的重复表达式,咱们可以给每个表达式生成一个唯一的规范键,用集合存起来,遇到重复的键就直接跳过,不生成重复表达式。
规范键怎么生成?
咱们搞个递归的规则:
- 单个数字的规范键就是它的字符串,比如
"2"、"3"; - 对于表达式
L op R:- 如果
op是交换律的(+、*):先拿到L和R的规范键,然后把两个键按字典序排序,再拼上运算符,比如2+3的规范键是"2+3",3+2的规范键也是"2+3",这样就标记成同一个表达式了; - 如果
op是非交换律的(-、/):直接按L的键+运算符+R的键生成,比如2-3的键是"2-3",3-2是"3-2",这俩是不同的表达式,保留各自的键。
- 如果
生成时的去重逻辑
递归生成的时候,咱们维护一个集合(比如Python里的set),每次生成新表达式前先算它的规范键,如果键已经在集合里了,就跳过;要是没见过,就把键加进集合,然后生成对应的表达式字符串。
比如:
- 当你尝试生成
3+2的时候,算出来规范键是"2+3",发现已经在集合里(之前生成2+3的时候加过了),直接跳过,不生成这个重复项; - 生成
5*(2+3)的时候,规范键是"2+3*5",和(2+3)*5的键一样,也直接跳过。
三、完整的递归生成+去重流程
把上面的逻辑串起来,完整的步骤大概是这样:
- 输入:给一组数字(比如
[2,3,5])和一组二元运算符(比如['+','*']); - 写个递归函数:比如叫
generate(nums),返回所有去重后的表达式(每个表达式要包含字符串、规范键、根运算符,根运算符用来判断括号); - 递归终止条件:如果数字列表只有一个元素,就返回这个数字的表达式信息,比如
[{"expr": "2", "key": "2", "root_op": None}]; - 拆分数字列表:遍历所有可能的拆分点,把当前数字列表拆成左右两个子列表,比如
[2,3,5]可以拆成[2]和[3,5],或者[2,3]和[5]; - 生成子表达式:递归调用
generate生成左右子列表的所有表达式; - 组合表达式:遍历每个左表达式、右表达式、运算符:
a. 先算当前表达式的规范键,按之前的规则来;
b. 检查规范键是否在集合里,在的话直接跳过;
c. 判断左右表达式要不要加括号:比如左表达式的根运算符优先级比当前运算符低,就给左表达式套括号;右表达式同理,还要考虑结合性;
d. 把左右表达式(加不加括号看情况)和运算符拼起来,记录根运算符,加入结果列表; - 返回结果:最后返回所有符合要求的表达式列表。
四、可选优化:处理结合律等价的重复(比如
a+(b+c)和(a+b)+c) 如果还想干掉结合律带来的重复,比如2+(3+5)和(2+3)+5,可以再优化规范键的生成:对于+、*这种结合律的运算符,把所有子表达式的规范键按字典序排序后拼接,比如2+3+5的规范键就是"2+3+5",不管括号怎么加,键都是一样的,这样就能避免生成这类重复表达式。
内容的提问来源于stack exchange,提问作者delphinarum
相关产品推荐
相关产品推荐

