A husband is a man of many miles.
——Unknown
1. 题目描述
2. 题目分析与解析
2.1 思路一——先算括号内的内容
这个题目其实就是编译原理中很小的一个模块了,基本思路还是通过栈来实现。题目的难点主要在:
其中括号优先级的处理,以及:
这两个特殊要求的处理。以及:
说明对于连续的字符串中的数字,我们应该把它当成一个数。
首先对于基本操作,跟上篇文章讲的一样(面试经典150题——逆波兰表达式求值),根据栈的特点进行运算。只不过对于 和 ()
需要进行额外处理。
对于空格,当然没必要加入栈,因为没什么操作,而如果碰见了 ()
,那么如果是左括号 (
就入栈,因为我们的括号计算是需要右括号 )
来界定截至位置的,所以在左括号之后仍然继续入栈。
当碰见 )
,不断出栈,直到碰见左括号,此时这一串内容就是需要计算的部分。
但是如果我们只使用一个栈来计算,就会出现一些难题:因为在上一篇内容中我们讲到计算是在入栈过程中判断当前字符是不是操作符来决定是否计算,但是此时因为有运算优先符,导致之前的内容已经在栈中,如果要计算需要先弹栈,所以就需要一个额外的栈作为每一个括号内部内容的计算临时栈。如下:
比如对于测试用例:
开始我们需要一直入栈直到出现下图情况:
此时发现右括号表示需要计算这一对括号中的内容,也就是(4+5+2):
那么我们就需要先把绿色部分弹栈,然后再按照上一篇文章讲的那样进行计算,得到结果11,入栈得到下述情况:
然后继续向后,此时又发现右括号:
继续按照上述操作进行计算,先弹栈到左括号,然后计算括号内部的内容,也就是在用一个栈计算,计算完毕再把结果放入这个栈中,得到如下:
再进行后续运算:
最后在所有内容计算完毕后,栈顶就是最终结果。
2.2 思路二——使用两个栈(操作数与运算符)
上面的思路一其实在一些更复杂的情况下比如出现了乘法除法或者大括号,处理起来会非常复杂,是因为对于不同优先运算的处理导致了我们初始定义的栈需要不断吐出数据然后计算又放回。而造成这个现象的本质原因是因为这些操作符我们放在了原始栈中,如果我们要计算就得从原始栈弹栈,那我们可不可以把操作符放在另一个栈中,操作数放在一个栈中,通过两个栈,同时在出现加减法时直接就进行运算(直到遇到左括号或者栈为空),就可以避免不断的弹栈的过程。比如还是对于这个测试用例:
在开始时,不断入栈,也要判断当前符号如果时加减法时,前面的已经计算过的内容是否能够计算,如果可以计算就需要先计算,因为这样可以避免数字的堆积,比如对于如下图的情况:
这时发现当前(黄色)为加/减运算,就要判定前面的内容是否可以计算,因为此时表达式为 (1+(4+5
,我们就先要把左括号后面的 4+5
先计算结果,然后将计算结果入栈操作数栈,当前运算符入栈运算符栈,得到如下:
然后遍历下一个字符,为2,出现下述情况:
继续向后,发现右括号,那么就不断弹栈计算直到操作符栈栈顶为 (
。其余操作基本类似。
但是还有一个注意的点,就是题目中说到了可能出现单元运算,对于这种情况,一种很巧妙的思想就是将单元运算转化为双元运算,比如对于 -1
,可以转化为 0 - 1
,对于 -(3 + 2)
可以转化为 0 - (3 + 2)
。
所以记住在编程过程中如果发现当前符号位 +/-
,要去判断当前位置的前一个字符,如果是 (,-,+
,说明这是一个单元运算,在将操作符加入栈之前还需要把操作数中先加入一个0,实现一元运算到二元运算的转换。
具体步骤在代码中有注释。
2.3 思路三
官方题解,这个题解利用了我们小学都学过的计算方式
-
比如对于1+(2+3),我们都知道等价于1+2+3
-
对于1-(2+3),我们都知道等价于1-2-3
-
也就是说对于括号前面是
+/-
,我们分别要对应对括号内部的内容进行不反转/反转
官方的题解就是利用了这种性质,通过一个标记位,标记当前括号内部的符号应不应该反转。
没看懂没关系,我会在代码中进行详细注释。
3. 代码实现
3.1 思路一
3.2 思路二
3.3 思路三
4. 相关复杂度分析
方法 1(calculate
)
-
时间复杂度:O(n),其中 n 是字符串的长度。字符串被遍历一次,每个字符最多进出栈一次。
-
空间复杂度:O(n)。使用了两个栈来存储字符和临时结果,最坏情况下,当所有字符都是数字或括号时,栈的大小可以增加到 O(n)。
方法 2(calculate2
)
-
时间复杂度:O(n)。与方法 1 类似,字符串被遍历一次,每个字符最多进出栈一次。
-
空间复杂度:O(n)。这里有两个栈:一个用于数字,一个用于操作符。在最坏的情况下,操作符和数字可能全部被存储在栈中,所以空间复杂度是 O(n)。
方法 3(calculate3
)
-
时间复杂度:O(n)。字符串仍然只遍历一次。
-
空间复杂度:O(n)。虽然只有一个操作符栈
ops
和一个用于迭代的sign
变量,但是在最坏情况下,当所有括号都嵌套时,栈的大小会与括号的数量成线性关系,因此空间复杂度为 O(n)。
在所有四种方法中,虽然时间复杂度都是 O(n),因为都是通过一次遍历来处理字符串,但是在实际的性能表现上可能会有细微的差别,这取决于栈操作和函数调用的开销。同样,尽管空间复杂度都是 O(n),不同方法中栈的实际大小也可能略有差异。例如,方法 3 最为节省空间,因为它仅仅使用了一个栈来追踪括号内部的正负号。