解法一:回溯法
题意可以转化为,现在有 2n 个位置,每个位置可以放置 ( 或者 ),所有括号组合中,有多少个是合法的?
生成括号组合的思路其实就和全排列一致,暴力穷举就行了
判断有效括号的题解可以参考这篇文章,主要是借助栈
func generateParenthesis(n int) []string {
var res []string
choiceList := []string{"(", ")"}
backtrack(choiceList, n, "", &res)
return res
}
func backtrack(choiceList []string, n int, combination string, res *[]string){
if len(combination) == 2*n{
if isValid(combination){ // 合法的括号才放入结果集
*res = append(*res, combination)
}
return
}
for _, choice := range choiceList{
combination+=choice // 选择一个括号加入子集
backtrack(choiceList, n, combination, res)
combination = combination[:len(combination)-1] // 撤销选择
}
}
func isValid(str string) bool{
var leftStack []rune // 栈空间存放左括号
for _, c := range str {
if c == '('{ // 左括号压栈
leftStack = append(leftStack, c)
} else { // 右括号,寻找是否有匹配的左括号
if len(leftStack) > 0 {
leftStack = leftStack[:len(leftStack)-1] // 弹出栈顶左括号
} else {
return false // 无左括号可匹配
}
}
}
// 是否所有的左括号都被匹配了
return len(leftStack) == 0
}
时间复杂度分析
- 每个位置有 2 种选择,总共有
2*n个位置,因此生成所有可能组合的时间复杂度为 O(2^2n)。 - 对于每个生成的组合,使用栈验证其有效性的时间复杂度为 O(2n),因为需要遍历组合中的每个字符
由于需要对每个生成的组合逐一进行验证,所以总体时间复杂度为 O(2^2n * 2n) = O(n * 4^n)
优化
为了减少不必要的穷举,我们可以优化一下剪枝策略,在回溯过程中只递归生成有效括号,而不是先暴力穷举所有括号组合才去判断是否合法
分析合法括号串有以下性质:
- 左括号数量一定等于右括号数量
- 对于一个「合法」的括号字符串组合
p,必然对任何0 <= i < len(p)都有:子串p[0...i]中左括号的数量都大于或等于右括号的数量。因为从左往右算的话,肯定是左括号多嘛,到最后左右括号数量相等,才说明这个括号组合是合法的。
我们可以用 left 记录还可以使用多少个左括号,用 right 记录还可以使用多少个右括号
func generateParenthesis(n int) []string {
var res []string
if n == 0{
return res
}
backtrack(n, n, "", &res) // 可用的左括号和右括号数量初始为 n
return res
}
func backtrack(left, right int, path string, res *[]string){
if left > right{ // 若左括号剩下的多,说明不合法
return
}
if left < 0 || right < 0{ // 若左/右括号剩下数量小于0,说明不合法
return
}
if left == 0 && right == 0{ // 当所有括号都恰好用完时,得到一个合法的括号组合
*res = append(*res, path)
return
}
// 尝试放一个左括号
path += "("
backtrack(left-1, right, path, res)
path = path[:len(path)-1] // 撤消选择
// 尝试放一个右括号
path += ")"
backtrack(left, right-1, path, res)
path = path[:len(path)-1] // 撤消选择
}
时间复杂度分析
在回溯过程中根据这两个变量的大小关系进行剪枝,避免生成无效组合。