22. 括号生成
1.题目介绍
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
2.题解
2.1 暴力枚举
思路
我们利用递归枚举出n对括号(这里是n对,说明共有2n个括号)即共2^2n种情况,并在其中找到正确的括号匹配情况。
1.递归返回条件,一次次加入'('和')'后,最终字符串长度到达n之后,进行合法性检验,若正确,加入结果数组中
2.合法性检验,必须随时满足左边括号多于等于右边括号数量,最终两边括号数量必须相等。
3.递归过程,现将当前字符串加上'(',继续递归,后删除这个'(',再加上')'进行继续递归。
4.注意条件给的是n对,所以最开始填参数时,总数量应为2*n;
代码
class Solution {
private:
bool vaild(string &curr){
int balance = 0;
for(char ch:curr){
if (ch == '(') balance++;
else balance--;
if(balance < 0) return false;
}
return balance == 0;
}
void generateAll(vector<string> &ans, int n, string ¤t){
if(n == current.size()){
if(vaild(current)){
ans.push_back(current);
}
return;
}
current += '(';
generateAll(ans,n,current);
current.pop_back();
current += ')';
generateAll(ans,n,current);
current.pop_back();
}
public:
vector<string> generateParenthesis(int n) {
vector<string> ans;
string current;
generateAll(ans, 2*n, current);
return ans;
}
};
复杂度分析
2.2 回溯法
思路
方法2.1有一个问题,就是当已经出现了不满足情况的括号组合,他依旧会继续递归下去,直到达到总括号数才会进行合法性检验,这样就浪费了很多递归资源
我们这里在每次递归中加入两个标记位置,分别标记已有的左括号数和有括号数目,左括号数目open < n(总共2n,左边的肯定要<n, =n的时候再加就超出了),有括号数目close < open < n即可。
这里所谓的回溯就是,在遇到不满足递归条件的中途节点,不会继续递归,而是回溯到上一级节点。这里就是使用if判断直接跳过本次递归过程,函数完成后自动回溯上一级调用节点。
代码
class Solution {
private:
void backtrack(vector<string> &ans, int n, int open, int close, string &curr){
if(n*2 == curr.size()){
ans.push_back(curr);
return;
}
if(open < n){
curr += '(';
backtrack(ans, n, open + 1, close, curr);
curr.pop_back();
}
if(close < open){
curr += ')';
backtrack(ans, n, open, close + 1, curr);
curr.pop_back();
}
}
public:
vector<string> generateParenthesis(int n) {
vector<string> ans;
string curr;
backtrack(ans, n, 0, 0, curr);
return ans;
}
};
复杂度分析
2.3 按括号序列的长度递归
思路
这里我们设计了一个函数generate,用于生成在长度为n(2*n)时,所有可能的合法括号序列。
任何一个合法的括号序列可以拆分为(a)b的结构,将一个大问题拆分为子问题,然后不断进行递归调用即可。
1.这里为何要使用共享指针shared_ptr?
1.1 主要是为了实现动态规划中的记忆化搜索来避免重复计算,将中间结果存储在缓存中,可以避免在递归过程中对相同问题的重复计算.
if(cache[n] != nullptr){ return cache[n];} 若结果成立,说明已存储中间结果,直接返回。这里便是减少了大量重复递归应用,节省资源时间。
举个例子:某一层递归 n = 8, i = 2, n -i -1 = 5; 递归得到了 2 和 5 对应的括号序列
之后再继续到了 i = 5, n - i - 1 = 2, 此时进入到2和5的递归,在指针数组中发现已经计算过了,直接返回即可!
1.2 可以灵活地分配和释放内存,只在需要时分配,避免不必要的空间占用
1.3 shared_ptr 的引用计数机制确保了在不再需要缓存时正确释放相关内存,比如像这里我在循环结束后并不希望释放指针result,而cache[n]与result指向同一块区域,虽然result消亡,但是cache[n]依然存在,引用计数不为零,并不会释放这个指针。
代码
class Solution {
shared_ptr<vector<string>> cache[9] = {nullptr}; //这里虽然1<=n<=8,但是数组从索引0开始,所以要多加一个
public:
shared_ptr<vector<string>> generate(int n) {
if(cache[n] != nullptr){ return cache[n];} //递归剪枝
if(n == 0) cache[0] = shared_ptr<vector<string>>(new vector<string>{""}); //当长度为0时,即一个空字符串,且必须是空字符串,空数组和空字符串是有区别的,空字符串数组中的一个元素,空数组是没有元素的!!!
else{
auto result = shared_ptr<vector<string>>(new vector<string>); //这里的字符串数组无需初始化,后面必定会进行添加的。(使用 result-> 解引用)
/* 开始讨论(a)b中a和b的各种情况*/
for(int i = 0; i != n; i++){
auto lefts = generate(i); // 指针指向->a长度为i(2*i)时各种可能的合法情况集合
// 指针指向->b长度为n-i-1(2*(n-i-1))时各种可能的合法情况集合(这里的相当于 2*n(总括号数目)- 2((a)b中的两个括号) - 2 * i(a的括号数目) / 2)
auto rights = generate(n-i-1);
for(auto &left: *lefts){
for(auto &right: *rights){
//相当于(*result). 这里不能使用cache[n]->注意此时其还是nullptr,需要我们加入result元素进去!!!
result -> push_back('('+ left + ')' + right);
}
}
}
cache[n] = result; //当 result 超出作用域时,shared_ptr 会检查引用计数。由于 cache[n] 也指向相同的内存块,引用计数不为零,内存不会被释放。
}
return cache[n];
}
vector<string> generateParenthesis(int n) {
return *generate(n); // 注意这里generate(n)返回的是一个指向vector<string>的指针,我们要返回的是一个数组,所以加上*
}
};