一天一道力扣Hot100(37):深度优先算法--括号生成

LeetCode 22. 括号生成(Go 语言实现)

个人主页: > 我不会起名字322 < (欢迎各位大佬莅临😊)
其他栏目: > 技术栈学习笔记 <
其他栏目: > 力扣Hot100题目解析 <
其他栏目: > Go项目学习笔记 <




前言:

上一篇组合总和里,我总结出了回溯的"三要素":路径、选择列表、结束条件。当时我说"一旦想清楚这三样东西,剩下的就是套模板"。

今天这道括号生成 ,正好是检验这句话的好例子------它和组合总和长得很不一样,没有数组、没有 startIndex、没有求和,但本质上还是同一套东西。我们一起来看看,三要素在这道题里分别变成了什么。

  1. 首先我们说DFS到底是干了什么事:DFS 是一种遍历策略;当它不设终点、走遍决策树的所有分支时,就是在穷举。回溯题用 DFS 穷举所有决策路径,并在满足条件处收集答案
  2. 路径(已做的选择) ,这是回溯题要看的第一个数据。这道题的路径就是当前已经拼出来的括号字符串。和组合总和不同的是,这里的选择不是从数组里挑元素,而是每一步决定"放左括号还是放右括号"。
  3. 选择列表(当前可做的选择) ,这是回溯要看的第二个数据。这道题的选择列表很特殊------它不是一个数组,而是由当前状态动态决定的:能不能放左括号?能不能放右括号?取决于已经用了几个左括号、几个右括号。
  4. 结束条件(到达决策树的底层) ,这是回溯要看的第三个数据。这里就是括号用完了 ,也就是字符串长度达到了 2 * n,此时把路径写入结果集。

整体的一个模板还是这样的:

go 复制代码
func backtrack(路径, 选择列表) {
    if 满足结束条件 {
        结果集 = append(结果集, 路径的拷贝)
        return
    }
    for 选择 := 选择列表 {
        做选择          // 路径加入该选择
        backtrack(路径, 新的选择列表)
        撤销选择        // 路径移除该选择
    }
}

下面我们来看一道题目深入理解一下

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。

示例 1:

复制代码
输入:n = 3
输出:["((()))","(()())"(())()","()(())","()()()"]

示例 2:

复制代码
输入:n = 1
输出:["()"]

示例 3:

复制代码
输入:n = 0
输出:[""]

有效括号的条件

在动手写代码之前,先想清楚"什么样的括号组合才算有效"。有两个条件缺一不可:

  1. 数量相等 :左括号和右括号各 n 个,总数 2n
  2. 前缀合法:任意前缀中,左括号数量 ≥ 右括号数量

第 2 条尤其关键。比如 "())(" 虽然左括号右括号都是 2 个,但中途右括号一度超过了左括号,所以不合法。这个条件直接决定了我们的剪枝策略。

  • 首先,我们不需要排序,也不需要数组 。这道题的输入只有一个整数 n,没有候选数组,所以组合总和里的 sort.IntsstartIndex 在这里都用不上。这正说明:三要素是本质,具体形式随题目而变

  • 其次,我们要定义几个数据

    1. 一个是当前已经拼出来的括号字符串(路径)

      go 复制代码
      path := make([]byte, 0, 2*n) // 用 []byte 避免频繁字符串拼接
    2. 一个是结果集,用来写入结果

      go 复制代码
      result := []string{}
    3. 上面两个是确定的,最后一个看题目不同来自己确定。这道题我们需要记录已经用了几个左括号、几个右括号,用来判断下一步能放什么、以及什么时候结束

      go 复制代码
      open, close := 0, 0

      注意:这里不像组合总和那样传 curSum,因为括号题没有"和"要凑,取而代之的是两个计数器。

  • 有了上面的数据,我们现在来套用模板来写这道题目

    go 复制代码
    func backtrack(路径, 选择列表) {
    	首先是这个大框架,路径肯定要传参 path,选择列表呢?
    	这道题的选择列表不是数组,而是由 open 和 close 两个变量动态决定的:
    	- open < n 时,可以放 '('
    	- close < open 时,可以放 ')'
    }
    
    if 满足结束条件 {
        这里的满足条件就是 len(path) == 2*n
        
        这里的路径拷贝就是把当前符合答案标准的 path 转成 string 写入结果集
        结果集 = append(结果集, string(path))
        return
    }
    //这里我们要想,如果还能放括号怎么办呢?显然,继续下面的递归即可
    //那如果既不能放左括号、也不能放右括号呢?这种情况其实不会出现,
    //因为只要没到 2*n,open < n 或 close < open 至少有一个成立
    
    // 选择一:放左括号
    if open < n {
        path = append(path, '(')
        backtrack(open+1, close)
        path = path[:len(path)-1] // 撤销选择
    }
    
    // 选择二:放右括号(右括号不能多于左括号,这就是剪枝)
    if close < open {
        path = append(path, ')')
        backtrack(open, close+1)
        path = path[:len(path)-1] // 撤销选择
    }
  • 因此,我们最后改造的函数就是

    go 复制代码
    func generateParenthesis(n int) []string {
        result := []string{}
        path := make([]byte, 0, 2*n) // 预分配容量
        
        var backtrack func(open, close int)
        backtrack = func(open, close int) {
            // 结束条件:括号用完
            if len(path) == 2*n {
                result = append(result, string(path))
                return
            }
            
            // 选择一:加左括号
            if open < n {
                path = append(path, '(')
                backtrack(open+1, close)
                path = path[:len(path)-1] // 回溯
            }
            
            // 选择二:加右括号(右括号不能多于左括号,这就是剪枝)
            if close < open {
                path = append(path, ')')
                backtrack(open, close+1)
                path = path[:len(path)-1] // 回溯
            }
        }
        
        backtrack(0, 0)
        return result
    }

复杂度分析

  • 时间复杂度:O(4ⁿ / √n),即第 n 个卡特兰数
  • 空间复杂度:O(n),递归栈深度(不算结果存储)

总结

回过头看,这道题再次印证了"回溯三要素":

三要素 在括号生成里的体现
路径 path,记录当前拼出的括号串
选择列表 openclose 动态决定:能放 ( 吗?能放 ) 吗?
结束条件 len(path) == 2*n 时收集答案

和组合总和对比一下,区别一目了然:

组合总和 括号生成
路径 path []int path []byte
选择列表 startIndex 之后的数组元素 open/close 动态判断
结束条件 curSum == target len(path) == 2*n
剪枝 curSum + candidates[i] > target close < open

题目变了,形式变了,但三要素的骨架没变。这就是为什么我说"想清楚这三样,剩下的就是套模板"。

本文是 《算法题目解析系列》 的第 37 篇,本系列将持续更新,每篇都提供清晰的思路与编程语言实现。欢迎关注,第一时间获取更新。如果你有想看的题目,也可以在评论区留言告诉我。

相关推荐
大圣编蚕1 小时前
Java 运算符详解:赋值类运算符
java
m0_734571761 小时前
深入理解C++ 多态<三>(Polymorphism)模板方法模式
开发语言·c++
君顾11 小时前
24小时自助健身房系统开发实战:从0到1完整技术架构指南
java·开发语言·健身房
白远山1 小时前
城市电竞陪玩调度系统实战:从派单算法到多端协同的架构拆解
java·架构·uni-app·需求分析
小白勇闯网安圈1 小时前
第5章 流式输出与人工审核
python·langchain
foolishlee1 小时前
SCRAM-SHA-256
数据库·算法·postgresql
Rabitebla1 小时前
【Linux 系统编程】权限(一):身份、提权,和那 9 个权限位
linux·数据结构·c++·算法
打工仔折腾 AI1 小时前
数据库上 K8s 之后谁来管?拆解金仓 KES-Operator 的声明式运维方案
人工智能·后端·python·性能优化·ai agent 实战
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】SQLite基础概念与C的API开发
网络·c++·人工智能·学习·架构