Leetcode
个人学习记录,不做它用。
括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3
输出:[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/generate-parentheses
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
方法一(暴力法)
思路:
- 首先找出所有的以左括号开头,右括号结尾的不重复的序列,形成序列集S。
- 找出S中所有有效的序列并输出。
序列集S的形成方法:
为了依次输出所有不重复的序列,这里考虑采用树来存储所有分支,再以特定方式遍历树得到所有的序列。
第一步:生成树
很容易的发现,所有可能的序列最开始一定以左括号开头,这里可以提前筛选让暴力算法不是那么暴力。一个简单的递归,其中l_num和r_num的作用就是维持整体的括号数的一致,原因同上。
Node* create_treenode(char value, int l_num, int r_num)
{
//初始化结点
Node *node = (Node*)malloc(sizeof(Node));
node->value = value;
node->l_child = node->r_child=NULL;
node->l_num = l_num;
node->r_num = r_num;
//进入递归,如果还有可分配的左括号,则调用create_treenode('(', l_num-1, r_mum)
//右括号类似
if (l_num > 0 && node->l_child == NULL)
{
node->l_child = create_treenode('(', l_num-1, r_num);
}
if (r_num > 0 && node->r_child == NULL)
{
node->r_child = create_treenode(')', l_num, r_num-1);
}
//递归返回
if(l_num == 0 && r_num == 0){
types++;
}
return node;
}
第二步:通过树获取所有可能的序列,存入一个指针数组中
以一个第一步三对括号生成的树为例,从根结点到每个叶子结点的所有路径即为序列集S,可以看出应采用类似先序遍历的方式,先输出根结点,再向左或向右深入。
但是,问题是怎么在寻找完一个路径之后自动切换到下一个呢?这里考虑的方法是:每次只读出一条路径,去掉已读的部分,再次从根开始遍历即为另一路径。
首先,可以让程序判断,当前的结点如果左右子树都为空,则说明到了路径的末尾,那么就需要在输出值之后把自己free掉,为了保证free掉的结点变成NULL,特设立一个标志位flag,每次free掉一个结点就把flag置1,代表返回之后应该把原来的这个结点置为NULL,而后返回,把free掉的结点改为NULL,flag置0,继续循环。
但是如何只遍历一条路径且删除完不必要的结点之后自行退出呢?正常情况下,遍历该树会遍历完左子树之后遍历右子树,遍历右子树后自然退出,如果只遍历一条路径,绝对不能出现遍历完左子树之后遍历右子树的情况,所以遍历完左子树应该直接回溯,保证每次只走一条路。代码如下:
void print_to_array(Node *root, char array[], int array_startnum)
{
array[array_startnum] = root->value;
if(root->l_child != NULL){
print_to_array(root->l_child, array, array_startnum+1);
if(flag ==1){
root->l_child = NULL;
flag = 0;
}
if(root->r_child != NULL){
return;
}
}
if(root->r_child != NULL){
print_to_array(root->r_child, array, array_startnum+1);
if(flag == 1){
root->r_child = NULL;
flag = 0;
}
}
if(root->l_child == NULL && root->r_child ==NULL){
free(root);
flag = 1;
}
}
函数体第一行是将值输入的数组中存储,其余代码如上所述。
main函数调用部分如下所示:
int main(int argc, char const *argv[])
{
Node* root = (Node*)malloc(sizeof(Node));
root = create_treenode('(', n-1, n);
char array[2*n];
char* print_array[types];
int i = 0;
int j = 0;
for(i = 0; i < types; i++){
if(flag == 1){
root->l_child = NULL;
flag = 0;
}
print_to_array(root, array, 0);
print_array[i] = array;
for(j = 0;j < 2*n;j++){
printf("%c ",array[j]);
}
printf("\n");
}
system("pause");
return 0;
}
仅仅这样的话,其实仍然存在问题,我们可以注意到,flag=1需要回溯到上一次才能置NULL,那么对于根节点的孩子,如果它们左右子树都为空,返回上一层就是返回到了main函数,所以一定要在main函数中也做出置NULL的操作。
types的值在第一个代码块递归返回处体现,因为叶子结点数就是不同序列的个数。
完整代码如下:
#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#define n 4 //定义括号对数
//定义结点
typedef struct tree_node
{
char value;//符号内容
struct tree_node * l_child;//左孩子
int l_num;//可分配的剩余左括号的数量
struct tree_node * r_child;//右孩子
int r_num;//可分配的剩余右括号的数量
}Node;
static int types = 0;
static int flag = 0;
Node* create_treenode(char value, int l_num, int r_num)
{
//初始化结点
Node *node = (Node*)malloc(sizeof(Node));
node->value = value;
node->l_child = node->r_child=NULL;
node->l_num = l_num;
node->r_num = r_num;
//进入递归,如果还有可分配的左括号,则调用create_treenode('(', l_num-1, r_mum)
//右括号类似
if (l_num > 0 && node->l_child == NULL)
{
node->l_child = create_treenode('(', l_num-1, r_num);
}
if (r_num > 0 && node->r_child == NULL)
{
node->r_child = create_treenode(')', l_num, r_num-1);
}
//递归返回
if(l_num == 0 && r_num == 0){
types++;
}
return node;
}
//void print_treenode(Node* root)
//{
// if(root->l_child != NULL){
// print_treenode(root->l_child);
// }
// printf("%c",root->value);
// if(root->r_child != NULL){
// print_treenode(root->r_child);
// }
//}
void print_to_array(Node *root, char array[], int array_startnum)
{
array[array_startnum] = root->value;
if(root->l_child != NULL){
print_to_array(root->l_child, array, array_startnum+1);
if(flag ==1){
root->l_child = NULL;
flag = 0;
}
if(root->r_child != NULL){
return;
}
}
if(root->r_child != NULL){
print_to_array(root->r_child, array, array_startnum+1);
if(flag ==1){
root->r_child = NULL;
flag = 0;
}
}
if(root->l_child == NULL && root->r_child ==NULL){
free(root);
flag = 1;
}
}
int main(int argc, char const *argv[])
{
Node* root = (Node*)malloc(sizeof(Node));
root = create_treenode('(', n-1, n);
char array[2*n];
char* print_array[types];
int i = 0;
int j = 0;
for(i = 0; i < types; i++){
if(flag == 1){
root->l_child = NULL;
flag = 0;
}
print_to_array(root, array, 0);
print_array[i] = array;
for(j = 0;j < 2*n;j++){
printf("%c ",array[j]);
}
printf("\n");
}
//print_treenode(root);
//printf("\n");
//printf("%d\n",types);
system("pause");
return 0;
}
验证序列是否有效的方法:
方法一:
- 构造一个栈,初始时栈中有一元素 ‘#’ 。
- 遍历序列。
- 如果是左括号则入栈,继续遍历,否则检查栈中是否有左括号,有则弹出一个左括号,继续遍历,若无,则序列无效。
- 当序列遍历完成时,如果栈顶为 ‘#’ ,则说明序列有效,否则无效。
可以看出,如果序列长为m,最坏的情况下,栈所占用的空间为 m 2 + 1 \frac{m}{2}+1 2m+1字节(char类型占一字节)。
方法二:
将方法一抽象,每次遍历到第n个元素是右括号时,检查栈中是否存在左括号,实际就是在检查[0,n]这个子序列中二者个数关系(每次来一个右括号都会领一个左括号走,可以允许左括号等右括号,因为可能后面还有右括号,但不能允许右括号等左括号,因为这样一定有错误,所以左括号数量一定要大于等于右括号),在序列遍历完成之后,如果还有剩下的左括号,那么就是无效序列,因为不可能有右括号去匹配了。
在序列遍历完成之前,每次见到一个右括号,我们都需要检查二者数量是否相等,序列遍历完成之后,我们要看有没有剩余的左括号,不妨定义一个变量difference,每次遍历到一个左括号,我们就把difference加一,每次来一个右括号,我们就可以把difference减一,在每次减一之后,我们检查一下difference是否小于0,是则序列无效,否则继续遍历,遍历完成后,如果difference大于0,代表还剩左括号,序列无效,为0,刚好配对完成,序列有效。
验证方法比较简单,可以自行举例验证。