LeetCode 22. 括号生成(Go 语言实现)
个人主页: > 我不会起名字322 < (欢迎各位大佬莅临😊)
其他栏目: > 技术栈学习笔记 <
其他栏目: > 力扣Hot100题目解析 <
其他栏目: > Go项目学习笔记 <
前言:
上一篇组合总和里,我总结出了回溯的"三要素":路径、选择列表、结束条件。当时我说"一旦想清楚这三样东西,剩下的就是套模板"。
今天这道括号生成 ,正好是检验这句话的好例子------它和组合总和长得很不一样,没有数组、没有 startIndex、没有求和,但本质上还是同一套东西。我们一起来看看,三要素在这道题里分别变成了什么。
- 首先我们说DFS到底是干了什么事:DFS 是一种遍历策略;当它不设终点、走遍决策树的所有分支时,就是在穷举。回溯题用 DFS 穷举所有决策路径,并在满足条件处收集答案
- 路径(已做的选择) ,这是回溯题要看的第一个数据。这道题的路径就是当前已经拼出来的括号字符串。和组合总和不同的是,这里的选择不是从数组里挑元素,而是每一步决定"放左括号还是放右括号"。
- 选择列表(当前可做的选择) ,这是回溯要看的第二个数据。这道题的选择列表很特殊------它不是一个数组,而是由当前状态动态决定的:能不能放左括号?能不能放右括号?取决于已经用了几个左括号、几个右括号。
- 结束条件(到达决策树的底层) ,这是回溯要看的第三个数据。这里就是括号用完了 ,也就是字符串长度达到了
2 * n,此时把路径写入结果集。
整体的一个模板还是这样的:
go
func backtrack(路径, 选择列表) {
if 满足结束条件 {
结果集 = append(结果集, 路径的拷贝)
return
}
for 选择 := 选择列表 {
做选择 // 路径加入该选择
backtrack(路径, 新的选择列表)
撤销选择 // 路径移除该选择
}
}
下面我们来看一道题目深入理解一下
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())"(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
示例 3:
输入:n = 0
输出:[""]
有效括号的条件
在动手写代码之前,先想清楚"什么样的括号组合才算有效"。有两个条件缺一不可:
- 数量相等 :左括号和右括号各
n个,总数2n - 前缀合法:任意前缀中,左括号数量 ≥ 右括号数量
第 2 条尤其关键。比如 "())(" 虽然左括号右括号都是 2 个,但中途右括号一度超过了左括号,所以不合法。这个条件直接决定了我们的剪枝策略。
-
首先,我们不需要排序,也不需要数组 。这道题的输入只有一个整数
n,没有候选数组,所以组合总和里的sort.Ints和startIndex在这里都用不上。这正说明:三要素是本质,具体形式随题目而变。 -
其次,我们要定义几个数据
-
一个是当前已经拼出来的括号字符串(路径)
gopath := make([]byte, 0, 2*n) // 用 []byte 避免频繁字符串拼接 -
一个是结果集,用来写入结果
goresult := []string{} -
上面两个是确定的,最后一个看题目不同来自己确定。这道题我们需要记录已经用了几个左括号、几个右括号,用来判断下一步能放什么、以及什么时候结束
goopen, close := 0, 0注意:这里不像组合总和那样传
curSum,因为括号题没有"和"要凑,取而代之的是两个计数器。
-
-
有了上面的数据,我们现在来套用模板来写这道题目
gofunc 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] // 撤销选择 } -
因此,我们最后改造的函数就是
gofunc 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 篇,本系列将持续更新,每篇都提供清晰的思路与编程语言实现。欢迎关注,第一时间获取更新。如果你有想看的题目,也可以在评论区留言告诉我。