一天一道力扣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.Ints 和 startIndex 在这里都用不上。这正说明:三要素是本质,具体形式随题目而变。

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

    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,记录当前拼出的括号串
选择列表 由 open、close 动态决定:能放 ( 吗?能放 ) 吗?
结束条件 len(path) == 2*n 时收集答案

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

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

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

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

相关推荐
Sayai12 分钟前
MCP Server 开发实战:Spring AI 把后端日志查询暴露给 AI Agent(7 条设计原则与踩坑实录)
java·人工智能·ai agent·spring ai·mcp
大牧师18 分钟前
Mybatis 框架教程
java·数据库·mybatis·数据持久化·orm
SunnyDays101119 分钟前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java·开发语言·合并pdf
INGNIGHT20 分钟前
944 · 最大子矩阵(前缀和)
数据结构·c++·算法
金銀銅鐵33 分钟前
[Java] 用GUI展示class文件顶层的 access_flags
后端·python·ai编程
源码宝1 小时前
云HIS系统源码,HIS全套源码,支持监管系统对接与二次开发
java·源码·电子病历·云his系统·程序代码·全套源码
kimnoic1 小时前
Python操作xlwings的实例详解
开发语言·python
萧瑟余晖1 小时前
Spring MVC拦截器异常处理与跨域详解
java·spring·mvc
2601_957883841 小时前
2026年10月 外星人笔记本维修须知
python·电脑