给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
如n=3,结果应为
[ "((()))", "(()())", "(())()", "()(())", "()()()" ]
这是一个明显的排列问题,要穷尽所有的组合,可以考虑回溯法,用回溯法要考虑以下几个问题
①终止条件:可以根据当前字符串长度来进行判断,若为n的2倍则可以终止;然而每一次递归都求一次长度会不会效率过于低下,所以可以考虑进行计数:每匹配一次,n自减1,减到0就可以终止了
②递归情况:很明显,对于每一个节点,最多有两个子节点:+"("和+")",但是不是每一个节点都能有两个子节点,一是考虑"("已经满n个了的情况,这种情况只能添加")",不能加"(",二是"("已经被匹配完了的情况,这种情况只能加"(",因为不能")"开头,剩余的情况都是两个都能加的了
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ret = []
def test(s, n, i, j):
if n == 0:
ret.append(s)
else:
if j == m:
s += ")"
test(s, n-1, i-1, j)
elif i == 0:
s += "("
test(s, n, i+1, j+1)
else:
s += "("
test(s, n, i+1, j+1)
s = s[:-1] + ")"
test(s, n-1, i-1, j)
m = n
test("", n ,0 , 0)
return ret
用时44ms,99.37%
然后尝试把s+"("这种操作移到递归的调用中,根据前面的文章 电话号码的字母组合(python)&回溯法以及递归的理解 中的想法,这样做必然降低速度,但是能提高代码可看性
def generateParenthesis(self, n: int) -> List[str]:
ret = []
def test(s, n, i, j):
if n == 0:
ret.append(s)
else:
if j == m:
test(s+")", n-1, i-1, j)
elif i == 0:
test(s+"(", n, i+1, j+1)
else:
test(s+"(", n, i+1, j+1)
test(s+")", n-1, i-1, j)
m = n
test("", n ,0 , 0)
return ret
用时52ms,94.99%
然后看来一下别人的答案,代码更加简洁,他们讨论的是未满可加(我是先讨论已满只能加,剩下的情况自由加两个符号之一,所以我要比他们多一种情况,他们的代码更加简洁,但是由于我讨论的情况之间互斥,所以我的代码效率会更高)
def generateParenthesis(self, n: int) -> List[str]:
ret = []
def test(s, n, i, j):
if n == 0:
ret.append(s)
if i < m:
test(s+"(", n, i+1, j+1)
if j != 0:
test(s+")", n-1, i, j-1)
m = n
test("", n , 0 , 0)
return ret
56ms,91.18%
同理如果将s+"("这样的操作移到递归调用外,效率会高一点
def generateParenthesis(self, n: int) -> List[str]:
ret = []
def test(s, n, i, j):
if n == 0:
ret.append(s)
if i < m:
s += "("
test(s, n, i+1, j+1)
s = s[:-1]
if j != 0:
s += ")"
test(s, n-1, i, j-1)
m = n
test("", n , 0 , 0)
return ret
48ms,97.9%
最后总结一下回溯法的实现要点
①退出情况(优化点)
②递归情况:其实就是对每一个子节点进行递归调用
如到了a就要对def进行递归,有的子节点递归是循环实现,如电话号码组合的那个题,有的则需要对每个点进行单独递归,这是由每个节点的子节点的构成情况决定的。需要注意的是,如果将一个变量的处理放在了递归调用之外(也就是不在函数调用的参数中进行改变,那么在这个递归调用之后下一个递归调用之前,一定还要把这个变量变回去)
③优化,也就是变量是在递归调用外处理还是在递归调用的参数中进行处理,前者能适当提高效率,后者能够简化代码。