【问题标题】:balanced parantheses recursive method in scalascala中的平衡括号递归方法
【发布时间】:2021-04-14 19:45:04
【问题描述】:

我正在 scala 中进行函数式编程,但在尝试编写递归方法来找到平衡括号时有点难过。

免责声明 - 我不是在寻找复制/粘贴的答案,我只是希望有人能指出为什么我的条件语句没有评估(我对 scala 很陌生,所以条件本身就是表达式的想法是一个新概念对我来说)。

这是我目前的尝试:

  def balance(chars: List[Char]): Boolean = {
    val charStack = new Stack[Char]()

    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)
      }


    }
    balanceRec(chars)

  }

我正在测试一个非常简单的输入,比如val chars = List('(', 't', 'e', 's', 't', ')', 'a'),甚至仍然得到一个java.util.NoSuchElementException: head of empty list。

我对为什么我的第二个else if 语句没有触发返回false 感到困惑。另外,我已经充实了这些条件,以确保我理解它们按我认为的那样工作——请不要取笑我:)

【问题讨论】:

  • 因为您的条件没有连接到单个表达式中。例如,第一个if 与其他所有内容分开,第二组ifs 没有最终的else。我建议您改用模式匹配,但您可以通过将所有 if 加入单个表达式或使用 return (not recommended) 来解决此问题。 - 此外,尝试在没有任何可变性的情况下解决此问题,将您的堆栈作为另一个 List 保留在递归函数中应该非常简单。
  • 谢谢@LuisMiguelMejíaSuárez。实际上我刚才才意识到这个问题,但我肯定会尝试取消可变结构以及模式匹配。谢谢:)
  • @MasonEdmison 看看this question :)
  • “我对 scala 很陌生,所以条件表达式本身就是一个新概念对我来说是一个新概念”——请问你习惯什么语言?因为例如C、C++、Java、C#、ECMAScript、PHP 和许多其他语言都有 条件运算符 (?/:),这是一个表达式,Python 有一个条件表达式,Ruby 两者都有条件运算符 和 也是 if 也是一个表达式。所以,这根本不是一个非常独特的概念。
  • @JörgWMittag。在 python 中,您可以有一个 if 语句而没有对应的 else。我已经养成了以这种方式检查退化病例的习惯:if varA is None: return None; if varA == "varA": return "varA"; else: "some other variable name"

标签: scala recursion


【解决方案1】:

让我们从清理一下开始,以使发生的事情更容易。

错误明显在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
}

【讨论】:

  • 感谢您回答我的问题。我特别喜欢depth:Int 解决方案。我已经调整了我的代码以使用它来消除可变堆栈(似乎不赞成在 scala 中使用可变数据结构)并简化代码:)
【解决方案2】:

我只想指出,递归并非总是需要的,而且有时递归并不是最实用的方法。

如果我们只想确定括号是否平衡,我们只想知道( 和) 的数量是否相同。

编辑:这忘记了)( 将返回 true,尽管它是无效的!我已更新响应以解决此问题。

为了解决这个问题,我们可以考虑到总和必须始终大于零

我们可以使用map将Chars的列表转化为Ints的列表

我们可以将( 转换为+1,将) 转换为-1,以及我们不关心的所有其他字符,可以将它们转换为0

为了检查余额,我们的新列表的总和应该是0,并且值不能低于零

为此,我们可以使用foldLeft 操作符,它接受两个参数列表


val testList: List[Char] = List('(', 't', 'e', 's', 't', 'i', 'n', 'g', ')', '!')
    
val mappedList: List[Int] = testList.map{ currentChar =>
    if (currentChar == '(') 1
    else if (currentChar == ')') -1
    else 0
}
    

def foldOp(currentTotal: Int, newValue: Int) = {
    if (currentTotal < 0) {
        currentTotal
    } else {
        currentTotal + newValue
    }
}

val initialValue: Int = 0

val sumResult: Int = mappedList.foldLeft(initialValue)(foldOp)
    
val isBalanced: Boolean = sumResult == 0
    // true

作为一个函数,它可能看起来像:


def isBalanced( charList: List[Char] ): Boolean = {
    def mapChar( char: Char): Int = {
        if (char == '(') 1
        else if (char == ')') -1
        else 0
    }
    def foldOp(currentTotal: Int, newValue: Int) = {
        if (currentTotal < 0) {
            currentTotal
        } else {
            currentTotal + newValue
        }
    }
        
    val charToIntList: List[Int] = charList.map(mapChar)
    val sum: Int = charToIntList.foldLeft(0)(foldOp)
    sum == 0
}

模式匹配也是一个很棒的功能性工具:


def isBalanced( charList: List[Char] ): Boolean = {
    charList.map{
        case '(' => 1
        case ')' => -1
        case _ => 0
    }.foldLeft(0)(foldOp) == 0
}

我在这里的目标不是为您提供您应该复制/粘贴的答案,而是我想分享我自己的方法和对如何表达此类问题的解决方案的解释。我的目标是希望能启发解决方案,或了解解决问题的多种不同方法。

现在,话虽如此,这绝对可以递归完成:

def isBalanced( charList: List[Char], runningTotal: Int = 0 ): Boolean = {
    if (runningTotal < 0) {
        false
    }
    charList match {
            // Base Case (1) Empty List, return whether or not the total is 0
        case Nil => runningTotal == 0
            // Recursive Case head char and the rest of the list
        case headChar :: rest =>
            val mappedValue: Int = headChar match {
                case '(' => 1
                case ')' => -1
                case _ => 0
            }
            isBalanced(rest, runningTotal + mappedValue)
    }
}

最后,我还要指出filter。对于小列表,这是一种相当冗长的处理方式,但如果列表非常大,这将减少要检查的元素的大小(也许更适合更复杂的用例)

正如 Luis 指出的那样,collect 对此要好得多,它将像 map 一样应用部分函数(匹配)[_ match { case ... } 的语法糖],但对于任何未定义的情况,它不会收集起来。

def isBalanced( charList: List[Char] ): Boolean = {
    val mappedValues: List[Int] = charList.collect{
        case '(' => 1
        case ')' => -1
    }
    mappedValues.foldLeft(initialValue)(foldOp)
}

【讨论】:

  • @LuisMiguelMejíaSuárez 谢谢 :) 我想我已经解决了你的两个观点
  • 现在“唯一的问题” 是这是次优的,创建中间值并且在算法知道答案不平衡时不会立即停止。使递归成为解决此问题的“最佳”(TM)方法;)
  • @LuisMiguelMejíaSuárez 感谢您的意见,我们的目标不是最优的。目标是明确和明确的。 vals 旨在告知读者该数据代表什么。这是一个明确的练习,目的是明确的,并传授函数式编程中常见的想法和概念。
  • 与任何事情一样,经验伴随着实践。识别模式以及如何将事物应用于这些模式是学习 Scala 和学习函数式编程的基本部分。在共享代码方面,我非常坚信“每一行应该做一件事”的原则。它使调试变得更加容易,并且使错误的代码更易于共享和理解。在调试或学习时,理解做事的明确方式会导致对如何更聪明地做事的隐含理解。
  • 当然,从递归开始是理解更高级事物的第一步,例如map,它是根据foldLeft 实现的,它是根据递归实现的。 - 请注意,这个问题来自 Martin 的 Scala 第一堂课第一周的第一个练习。 - 保持简单对于刚刚开始理解表达式的 OP 来说更好;)
猜你喜欢
  • 2013-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-15
  • 2015-10-18
  • 2011-02-12
  • 1970-01-01
  • 2018-06-10
相关资源
最近更新 更多