让我们从清理一下开始,以使发生的事情更容易。
错误明显在balanceRec之内,所以我们甚至不用看balance:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
println(chars.mkString(", "))
if (chars.head == ')' &&
charStack.isEmpty
) {
false
}
else if (chars.head == ')' &&
charStack.pop() != '('
) {
false
}
else{
if(chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
接下来,println 显然也不是问题的一部分,所以让我们删除它:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
if (chars.head == ')' &&
charStack.isEmpty
) {
false
}
else if (chars.head == ')' &&
charStack.pop() != '('
) {
false
}
else{
if(chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
进一步,让我们删除一些不必要的括号并清理空白和缩进:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
现在方法的结构变得更加清晰了。
我们现在注意到,第一个表达式是一个 NO-OP:它没有副作用,它不改变任何东西,它不打印任何东西,它的结果不返回,它的结果不打印,其结果未分配给变量或字段。它实际上什么都不做。
这是主要问题。
事实证明,所有我们需要做的是在第二个 if 之前插入一个 else 关键字:
def balanceRec(chars: List[Char]): Boolean = {
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
}
[Scastie link]
由于我们方法的主体现在是一个表达式,我们可以去掉花括号:
def balanceRec(chars: List[Char]): Boolean =
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
最后,一些一般性提示。
您实际上并没有在代码中使用List 的任何特定功能,您只是在使用Seq 的更一般的功能,实际上,您实际上只是在使用Iterable 的功能。我更喜欢保持我的接口尽可能通用和通用,所以我会将这里的类型更改为Iterable:
def balance(chars: Iterable[Char]): Boolean = {
val charStack = new Stack[Char]()
def balanceRec(chars: Iterable[Char]): Boolean =
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
balanceRec(chars)
}
接下来,我觉得这个方法不好用,因为我必须传入一个单独的字符集合,而我会发现传入一个字符串更自然:
def balance(s: String): Boolean = {
val charStack = new Stack[Char]()
def balanceRec(chars: Iterable[Char]): Boolean =
if (chars.isEmpty) charStack.isEmpty
else if (chars.head == ')' && charStack.isEmpty) false
else if (chars.head == ')' && charStack.pop() != '(') false
else {
if (chars.head == '(') charStack.push(chars.head)
balanceRec(chars.tail)
}
balanceRec(s.toIterable)
}
但实际上String 也支持head 和tail,所以这里不需要使用Iterable:
def balance(s: String): Boolean = {
val charStack = new Stack[Char]()
def balanceRec(s: String): Boolean =
if (s.isEmpty) charStack.isEmpty
else if (s.head == ')' && charStack.isEmpty) false
else if (s.head == ')' && charStack.pop() != '(') false
else {
if (s.head == '(') charStack.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
我也更喜欢使用工厂方法而不是构造函数,所以我会像这样初始化堆栈:
val charStack = Stack.empty[Char]
在函数式编程中,与许多其他社区不同,短变量名是完全可以接受的,只要代码仍然可读。此外,我们通常信任我们的类型,例如,您不必说charStack:我们知道它是一个堆栈,因为它的类型为Stack!命名事物集合的典型命名模式是仅附加一个s 来表示复数,因此我们可以表示这个chars 甚至只是cs(巧合的是,也可以读作“char stack”的缩写)。
def balance(s: String): Boolean = {
val cs = Stack.empty[Char]
def balanceRec(s: String): Boolean =
if (s.isEmpty) cs.isEmpty
else if (s.head == ')' && cs.isEmpty) false
else if (s.head == ')' && cs.pop() != '(') false
else {
if (s.head == '(') cs.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
现在我们已经大大缩短了代码,我们注意到第二个和第三个替代方案(前两个elses)具有相同的结果:它们都评估为false。因此,我们可以简单地将它们的条件与 OR 连接起来:
def balance(s: String): Boolean = {
val cs = Stack.empty[Char]
def balanceRec(s: String): Boolean =
if (s.isEmpty) cs.isEmpty
else if (s.head == ')' && cs.isEmpty || s.head == ')' && cs.pop() != '(') false
else {
if (s.head == '(') cs.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
现在我们可以应用一些布尔代数并分解出s.head =='(' 条件:
def balance(s: String): Boolean = {
val cs = collection.mutable.Stack.empty[Char]
def balanceRec(s: String): Boolean =
if (s.isEmpty) cs.isEmpty
else if (s.head == ')' && (cs.isEmpty || cs.pop() != '(')) false
else {
if (s.head == '(') cs.push(s.head)
balanceRec(s.tail)
}
balanceRec(s)
}
最后,不需要可变累加器来在不同的递归调用之间传递信息,因为该语言已经为我们提供了允许我们将信息传入和传出调用的工具:参数和返回值!
因此,我们可以简单地传递一个不可变堆栈(为此我们将简单地使用List)作为参数,而不是将我们的状态信息保存在可变堆栈中,并且将其作为值返回:
def balance(s: String): Boolean = {
def balanceRec(s: String, cs: List[Char] = Nil): (Boolean, List[Char]) =
if (s.isEmpty) (cs.isEmpty, cs)
else if (s.head == ')' && cs.isEmpty || s.head == ')' && cs.head != '(') (false, cs)
else balanceRec(s.tail, if (s.head == '(') s.head :: cs else cs)
balanceRec(s)._1
}
请注意,与我上面所说的不同,我们在这里使用List[Char] 而不是Seq[Char]、Iterable[Char] 或String,因为在这种情况下我们确实使用了一个List的特征,即O(1) prepend, head, and tail,其他三个都不能保证。
!!!另请注意,此代码是错误的!!!
我们没有考虑到您的原始代码使用了副作用,更准确地说是变异。例如,第二个分支,即使不被采用,仍可能导致pop()ping 栈顶元素。让我重复一遍:未采用的分支仍然会改变结果。
特别是,如果s.head == ')' 是true 并且cs.isEmpty 是false 并且cs.head != '(' 是false(换句话说,cs.head == '(' 是true),那么我们pop()最顶层的元素,即使我们不采用这个分支,这个突变仍然在 if / else 链的后续分支以及未来的递归调用中可见。
因此,我们必须以与最终else 中类似的方式来考虑这一点:
def balance(s: String): Boolean = {
def balanceRec(s: String, cs: List[Char] = Nil): (Boolean, List[Char]) =
if (s.isEmpty) (cs.isEmpty, cs)
else if (s.head == ')' && cs.isEmpty) (false, cs)
else if (s.head == ')' && cs.head != '(') (false, cs.tail)
else if (s.head == ')' && cs.head == '(') balanceRec(s.tail, if (s.head == '(') s.head :: cs.tail else cs.tail)
else balanceRec(s.tail, if (s.head == '(') s.head :: cs else cs)
balanceRec(s)._1
}
现在,您可能会看到这个,然后说:“我认为函数式编程应该如此简单和美丽,但我在这里看到的是,当我们将可变解决方案转换为不可变解决方案时,它变成了三重嵌套逻辑的复杂混乱”。但事情是这样的:可变版本有完全相同的混乱!您只是没有看到它,因为由可变堆栈连接的代码的不同部分之间的依赖关系是不可见的。但他们还在那里。
作为替代解决方案,请注意我们实际上根本不需要知道堆栈的内容。我们只需要知道堆栈有多深!所以,我们可以这样做:
def balance(s: String): Boolean = {
def balanceRec(cs: List[Char], depth: Int = 0): Int =
if (depth < 0) depth
else cs match {
case Nil => depth
case '(' :: cs => balanceRec(cs, depth + 1)
case ')' :: cs => balanceRec(cs, depth - 1)
case c :: cs => balanceRec(cs, depth)
}
balanceRec(s.toList) == 0
}