LeedCode刷题笔记:使用递归、直接循环实现不同二叉搜索树

刷题还是得每天坚持,中间一旦停了几天,再写起来手生的不行。今天尝试写了一道之前写过的题:不同二叉搜索树,虽然之前的思路还有印象,但还是有好多地方报错,改了好一会儿。还是要每日坚持!

思路

思路还是蛮容易懂的,依照二叉搜索树:左<根<右的特点,在1-n之间选每一个节点作为根节点,假设为i,则(1,i-1)为左子树的节点,(i+1,n)右子树节点,分别计算出左右两边可以构造的二叉搜索树的数量,然后左右相乘可得最终结果。

实现

1.递归方法

我刚开始想的方法是递归:

int f(int left, int right) {
    int sum = 0;
    int i;
    if (left > right)
        return 1;
    if (left == right)
        return 1;
    for (i = left; i <= right; i++) {
        sum += f(left, i - 1) * f(i + 1, right);
    }
    return sum;
}
int numTrees(int n){
    return f(1,n);
}

但是运行的时间比较慢,然后我去看了官方的题解,数学推导的过程还是比较直观的。

2.循环方法1

思路仍然和递归的方法一样,我们设g[n]为n个节点构造的二叉搜索树的个数,如果n为1或0,那么显然个g[0]=g[1]=1,即只有一种构造方法。如果n>=2,假设n=6,节点:1、 2、3、4、5,6。

选取其中一个节点 i 为根节点,令此时可以构造的二叉搜索树为f(i,6)。

假设 i 选3,作为根节点,则左子树里的节点为1、 2,左子树可以构造的二叉搜索树个数为g[2];右子树:4、 5、 6,可以构造的二叉搜索树个数为g[3];所以 f(i,n)=f(3,6)=g[2]*g[3]=g[i-1]*g[n-i]。

那么依次选每一个节点为根节点,都有 f(i,n)=  g[i-1]*g[n-i]。

每一种结果求和,即可得到\sum_{i=1}^{n}g[i-1]*g[n-i]

以下是实现的代码:

int numTrees(int n) {
    int G[n+1];
    memset(G, 0, sizeof(G));
    G[0]=G[1]=1;
    //注意这里的i对应算法公式里的n,j对应i!
    for(int i=2;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
        {
            G[i]+=G[j-1]*G[i-j];
        }
    }
    return G[n];

    }

3.循环方法2

其实到这里还没完!还有高手!我们刚刚的公式还可以再推导:

这是官方题解给的卡塔兰数,所以我们的二层循环可以依此公式优化为一层:

int numTrees(int n) {
    
    int G[n+1];
    memset(G, 0, sizeof(G));
    G[0]=G[1]=1;
    for(int i=1;i<=n;i++)
    {
        int k=i-1;
        G[i]=G[k]*2*(2*k+1)/(k+2);
    }
    return G[n];

 }

果然!我又踩了两个坑!

1.这个公式已经直观的体现了Cn+1和Cn的关系,还需要使用数组保存吗?不需要了,直接用一个变量 c 来不断迭代就可以了。

2.这个变量 c 直接定义为int吗?看清楚公式里有乘法,一不注意就会越界!所以定义为 long long c!

代码如下:

int numTrees(int n) {
    
    long long c=1;
    int k;
    for(int i=1;i<=n;i++)
    {
        k=i-1;
        c=c*2*(2*k+1)/(k+2);
    }
    return c;

 }

运行时间和空间分析

这道题难度中等,但是写完三个解决方法,学到了很多。如图依次是递归、二层循环、一层循环运行的时间和空间分析:

1.递归方法最慢,使用空间最多。因为算法的原理并未改进,使用的就是左部分、右部分的节点构造的二叉搜索树个数相乘得到最后结果,并且每递归一层就要开辟left、right、sum的空间。

2.二层循环运行时间明显减少,速度提升非常多,这是因为,我们计算完每一种n的值都存到数组G中,后面需要这个值的时候直接取出来就用,运行速度就会特别快。

3.一层循环最优秀,时间快,使用空间少,这是因为卡塔兰数简化了算法,给出了n和n+1的结果之间的关系,所以无需数组保存,Cn算出来之后又被立马使用到Cn+1的计算当中。所以又快又省空间。

所以说刷题如果说追求的是把题做出来,也许没那么难?但是要找到一个又快又省的方法却需要很多的知识储备!

报错解决&知识积累

1.变长数组的初始化

写二层循环方法时遇到了一个问题,变长数组的初始化问题,我是这么写的:

 int G[n+1]=0;

但是报错:error: variable-sized object may not be initialized(变量大小的对象不能初始化)

这其实不算是严格错误,因为有的版本的编译器是这么写是可以通过的。但是为避免出现错误,我建议在给变长数组初始化为0时,使用memset函数,以下是menset的简要介绍:

memset本来是为char数组初始化准备的。

函数模板:
memset(数组首地址,初始值,初始化大小)

数组首地址:数组的首地址是可以直接用数组名代替的

初始值: 初始值的范围是0到127,因为memset是以字节为单位对数组进行赋值的,一个字节8位,所表达的值就是0到127

初始化大小: 初始化大小用一个整数表示,表示需要初始化多少字节,通常用sizeof(数组首地址)来获得需要初始化的大小

char arr[10]; 	
memset(arr,'a',10);//从数组首地址开始,初始化10个字节的值为'a';

memset(arr+2,'b',4);//从数组首地址+2开始,即arr[2]的地址开始,初始化4个字节的值为'b';
	
memset(arr,'c',sizeof arr);//从数组首地址开始,初始化数组大小个字节的值为'c',sizeof可以获取到数组的大小,这个代码中sizeof arr的值就是10  

依次结果为:

memset(arr,'a',10)后,数组的值为:
a a a a a a a a a a

memset(arr+2,'b',4)后,数组的值为:
a a b b b b a a a a

sizeof arr的值为10
memset(arr,'c',sizeof arr)后,数组的值为:
c c c c c c c c c c

所以,如果我们数组初始化的数值在0-127之间,就可以使用memset函数。如下所示:

memset(G,0,sizeof(G));

2、数值溢出问题

在一个赋值的右边有乘法或者乘方,一定要注意赋值符号的左值的类型,不然很容易溢出。虽然这个问题比较容易改正,但是还是值得注意。

  • 23
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我要好好学cs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值