DS博客作业03--树
0.PTA得分截图
1.本周学习总结
1.1 总结树及串内容
1.1.1 串相关
串的基本概念
串是线性表的一种,是由零个或多个字符组成的有限序列。串中所含字符的个数称为该串的长度,含零个字符的串称为空串。
串的存储结构
串中元素逻辑关系与线性表的相同,串可以采用与线性表相同的存储结构,即分为顺序和链式存储结构。
顺序串的定义如下:
#define MaxSize 100
typedef struct
{ char data[MaxSize];
int length;
} SqString;
链串中的一个结点可以存储多个字符。通常将链串中每个结点所存储的字符个数称为结点大小。
链串结点大小1时,链串的结点类型定义如下:
typedef struct snode
{ char data;
struct snode *next;
} LinkStrNode;
串的模式匹配之BF算法
- BF算法采用穷举的思路,从s的每一个字符开始依次与t的字符进行匹配。
BF算法代码如下:
int index(SqString s,SqString t)
{ int i=0, j=0;
while (i<s.length && j<t.length)
{ if (s.data[i]==t.data[j]) //继续匹配下一个字符
{ i++; //主串和子串依次匹配下一个字符
j++;
}
else //主串、子串指针回溯重新开始下一次匹配
{ i=i-j+1; //主串从下一个位置开始匹配
j=0; //子串从头开始匹配
}
}
if (j>=t.length)
return(i-t.length); //返回匹配的第一个字符的下标
else
return(-1); //模式匹配不成功
}
BF算法分析
算法在字符比较不相等,需要回溯(即i=i-j+1):即退到s中的下一个字符开始进行继续匹配
最好情况下的时间复杂度为O(m)
最坏情况下的时间复杂度为O(n×m)
平均的时间复杂度为O(n×m)
串的模式匹配之KMP算法
- KMP算法较BF算法有较大改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
next[j]指的是t[j]字符前有多少个字符与t开头的字符相同。
next[j]的含义
1.next[j]=k表示什么信息?
说明模式串t[j]之前有k个字符已成功匹配,下一趟应从t[k]开始匹配。
2.next[j]=-1表示什么信息?
说明模式串t[j]之前没有任何用于加速匹配的信息,下一趟应从t的开头即j++ j=0开始匹配。
KMP算法代码如下:
int KMPIndex(SqString s,SqString t)
{ int next[MaxSize], i=0, j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{ i++;
j++; //i、j各增1
}
else j=next[j]; //i不变,j后退
}
if (j>=t.length)
return(i-t.length); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
KMP算法分析
设串s的长度为n,串t长度为m
1.在KMP算法中求next数组的时间复杂度为O(m),在后面的匹配中因主串s的下标不减即不回溯,比较次数可记为n,所以KMP算法平均时间复杂度为O(n+m)
2.最坏的时间复杂度为O(n × m)
KMP算法的改进
在KMP算法的匹配中,依然存在缺陷,即相同的字符出现重复匹配的问题。因此,我们将KMP算法进一步改进。
将next
改为nextval
,进一步提高模式匹配的效率。
string类(C++)
优点:不必担心内存是否足够、字符串长度等,它包含了很多实用的函数
头文件:#include
声明:string 字符串名
输入区别:getline(cin,s):读取一行字符串;cin>>s:读取空格前的字符
1.1.2 树相关
树的定义
- 树是由n(n≥0)个结点组成的有限集合(记为T)。其中:
- 如果n=0,它是一棵空树,这是树的特例;
- 如果n>0,这n个结点中存在一个唯一结点作为树的根结点(root),其余结点可分为m (m≥0)个互不相交的有限子集T1、T2、…、Tm,而每个子集本身又是一棵树,称为根结点root的子树。
树的基本术语
-
1.结点的度与树的度:
- 树中一个结点的子树的个数称为该结点的度。树中各结点的度的最大值称为树的度,通常将度为m的树称为m次树或者m叉树。
- 树中一个结点的子树的个数称为该结点的度。树中各结点的度的最大值称为树的度,通常将度为m的树称为m次树或者m叉树。
-
2.分支结点与叶结点:
- 度不为零的结点称为非终端结点,又叫分支结点。度为零的结点称为终端结点或叶结点(或叶子结点)。
- 度为1的结点称为单分支结点;度为2的结点称为双分支结点,依此类推。
-
3.孩子结点、双亲结点和兄弟结点:
- 在一棵树中,每个结点的后继,被称作该结点的孩子结点(或子女结点)。相应地,该结点被称作孩子结点的双亲结点(或父母结点)。具有同一双亲的孩子结点互为兄弟结点。
-
4.子孙结点和祖先结点:
- 在一棵树中,一个结点的所有子树中的结点称为该结点的子孙结点。
- 从根结点到达一个结点的路径上经过的所有结点被称作该结点的祖先结点。
-
5.结点的层次和树的高度:
- 树中的每个结点都处在一个层次上。结点的层次从树根开始定义,根结点为第1层,它的孩子结点为第2层,以此类推,一个结点所在的层次为其双亲结点所在的层次加1。
- 树中结点的最大层次称为树的高度(或树的深度)。
-
6.森林:
- n(n>0)个互不相交的树的集合称为森林。
- 只要把树的根结点删去就成了森林。反之,只要给n棵独立的树加上一个结点,并把这n棵树作为该结点的子树,则森林就变成了一颗树。
树的基本运算
树的遍历运算是指按某种方式访问树中的每一个结点且每一个结点只被访问一次。
-
先根遍历:
- 若树不空,则先访问根结点,然后依次先根遍历各棵子树。
- 若树不空,则先访问根结点,然后依次先根遍历各棵子树。
-
后根遍历:
- 若树不空,则先依次后根遍历各棵子树,然后访问根结点。
- 若树不空,则先依次后根遍历各棵子树,然后访问根结点。
-
层次遍历:
- 若树不空,则自上而下、自左至右访问树中每个结点。
- 若树不空,则自上而下、自左至右访问树中每个结点。
树的存储结构
- 双亲存储结构
结构体定义:
typedef struct
{ ElemType data; //结点的值
int parent; //指向双亲的位置
} PTree[MaxSize];
双亲存储结构寻找一个结点的双亲结点比较方便,但是对于寻找一个结点的孩子节点操作实现却不太方便。
- 孩子链存储结构
结构体定义:
typedef struct node
{ ElemType data; //结点的值
struct node *sons[MaxSons]; //指向孩子结点
} TSonNode;
孩子链存储结构寻找一个节点的孩子节点操作比较方便,但是要寻找一个结点的双亲结点就比较麻烦。
- 孩子兄弟链存储结构
结构体定义:
typedef struct tnode
{ ElemType data; //结点的值
struct tnode *hp; //指向兄弟
struct tnode *vp; //指向孩子结点
} TSBNode;
孩子兄弟链存储结构的每个结点固定只有两个指针域,若寻找双亲结点,会比较麻烦。
1.1.3 二叉树相关
二叉树的定义
- 二叉树是有限的结点集合,这个集合或者是空,或者由一个根结点和两棵互不相交的称为左子树和右子树的二叉树组成。
满二叉树
- 满二叉树中,所有分支结点都有双分结点,并且叶结点都集中在二叉树的最下一层。
- 高度为h的满二叉树恰好有(2^h)-1 个结点。
完全二叉树
- 完全二叉树实际上是对应的满二叉树删除叶结点层最右边若干个结点得到的。
- 在完全二叉树中,最多只有下面两层的结点的度数小于2,并且最下面一层的叶结点都依次排列在该层最左边的位置上。
二叉树的性质
- 非空二叉树上叶结点数等于双分支结点数加1。
- 非空二叉树上第i层上至多有2^(i-1)个结点(i≥1)。
- 高度为h的二叉树至多有(2^h)-1个结点(h≥1)。
二叉树的存储结构
-
完全二叉树
-
非完全二叉树
二叉树顺序存储结构的特点:
对于完全二叉树来说,其顺序存储是十分合适的。
对于一般的二叉树,特别是对于那些单分支结点较多的二叉树来说是很不合适的,因为可能只有少数存储单元被利用,特别是对退化的二叉树(即每个分支结点都是单分支的),空间浪费更是惊人。
在顺序存储结构中,找一个结点的双亲和孩子都很容易。
二叉树的链式存储结构
结构体定义:
typedef struct TNode *Position;
typedef Position BinTree; //二叉树类型
struct TNode{
ElementType Data; //结点数据
BinTree Left; //指向左子树
BinTree Right; //指向右子树
};
二叉树链式结构的存储特点:
除了指针外,二叉链比较节省存储空间。占用的存储空间与树形没有关系,只与树中结点个数有关。
在二叉链中,找一个结点的孩子很容易,但找其双亲不方便。
二叉树的创建
- 顺序存储结构转换成二叉链
BTree CreateBTree(string str,int i)
{
int len;
BTree bt;
bt=new TNode;
len=str.size();
if(i>len || i<=0) return NULL;
if(str[i]=='#') return NULL;
bt->data =str[i];
bt->lchild =CreateBTree(str,2*i);
bt->rchild =CreateBTree(str,2*i+1);
return bt;
}
- 先序遍历递归建二叉树
BTree CreatTree(string str, int &i)
{
BTree bt;
if (i > len - 1) return NULL;
if (str[i] == '#') return NULL;
bt = new BTNode;
bt->data = str[i];
bt->lchild = CreatTree(str, ++i);
bt->rchild = CreatTree(str, ++i);
return bt;
}
- 层次法建二叉树
void CreateBTree(BTree &BT,string str)
{ BTree T;int i=0;
queue<BTree> Q;
if( str[0]!='0' ) /*分配根结点单元,并将结点地址入队*/
{
BT =new BTNode;
BT->data = str[0];
BT->lchild=BT->rchild=NULL;
Q.push(BT);
}
else BT=NULL; /* 若第1个数据就是0,返回空树 */
while( !Q.empty())
{
T = Q.front();/*从队列中取出一结点地址*/
Q.pop();
i++;
if(str[i]=='0' ) T->lchild = NULL;
else
{ /*生成左孩子结点;新结点入队*/
T->lchild = new BTNode;
T->lchild->data = str[i];
T->lchild->lchild=T->lchild->rchild=NULL;
Q.push(T->lchild);
}
i++; /* 读入T的右孩子 */
if(str[i]=='0') T->rchild = NULL;
else
{ /*生成右孩子结点;新结点入队*/
T->rchild = new BTNode;;
T->rchild->data = str[i];
T->rchild->lchild=T->rchild->rchild=NULL;
Q.push(T->rchild);
}
} /* 结束while */
}
- 括号法建二叉树
void CreateBTNode(BTNode * &b,char *str)
{ //由str 二叉链b
BTNode *St[MaxSize], *p;
int top=-1, k , j=0;
char ch;
b=NULL; //建立的二叉链初始时为空
ch=str[j];
while (ch!='\0') //str未扫描完时循环
{ switch(ch)
{
case '(': top++; St[top]=p; k=1; break; //可能有左孩子结点,进栈
case ')': top--; break;
case ',': k=2; break; //后面为右孩子结点
default: //遇到结点值
p=new BTNode;
p->data=ch;
p->lchild=p->rchild=NULL;
if (b==NULL) //p为二叉树的根结点
b=p;
else //已建立二叉树根结点
{ switch(k)
{
case 1: St[top]->lchild=p; break;
case 2: St[top]->rchild=p; break;
}
}
}
j++; ch=str[j]; //继续扫描str
}
}
二叉树的遍历
- 先序遍历
访问根节点
先序遍历左子树
先序遍历右子树
先序序列的第一个结点是根结点
遍历代码如下:
void PreOrder(BTree bt)
{ if (bt!=NULL)
{ printf("%c ",bt->data); //访问根结点
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
}
- 中序遍历
中序遍历左子树
访问根结点
中序遍历右子树
中序序列的根结点位于中间位置,根结点的左边是左子树的结点,右边是右子树的结点
遍历代码如下:
void InOrder(BTree bt)
{ if (bt!=NULL)
{ InOrder(bt->lchild);
printf("%c ",bt->data); //访问根结点
InOrder(bt->rchild);
}
}
- 后序遍历
后序遍历左子树
后序遍历右子树
访问根结点
后序序列的最后一个结点是根结点
遍历代码如下:
void PostOrder(BTree bt)
{ if (bt!=NULL)
{ PostOrder(bt->lchild);
PostOrder(bt->rchild);
printf("%c ",bt->data); //访问根结点
}
}
- 层次遍历
遍历代码如下:
初始化队列,先将根节点进队。
while(队列不空)
{ 队列中出列一个节点*p,访问它;
若它有左孩子节点,将左孩子节点进队;
若它有右孩子,将右孩子进队。
}
二叉树与树、森林之间的转换
- 森林、树转换为二叉树
1.相邻兄弟节点加一水平连线
2.除了左孩子和叶子节点,删掉连线
3.水平连线以左边节点轴心旋转45度
- 二叉树还原为一颗树
1.任一节点k,搜索所有右孩子
2.删掉右孩子连线
3.若节点k父亲为k0,则k0和所有k右孩子连线
二叉树的构造
- 先序和中序构造二叉树
BTRee CreateBT1(char *pre,char *in,int n)
{ BTNode *s; char *p; int k;
if (n<=0) return NULL;
s=new BTNode;
s->data=*pre; //创建根节点
for (p=in;p<in+n;p++) //在中序中找为*ppos的位置k
if (*p==*pre)
break;
k=p-in;
s->lchild=CreateBT1(pre+1,in,k); //构造左子树
s->rchild=CreateBT1(pre+k+1,p+1,n-k-1); //右子树
return s;
}
- 后序和中序构造二叉树
BTRee CreateBT2(char *post,char *in,int n)
{ BTNode *s; char *p; int k;
if (n<=0) return NULL;
s=new BTNode;//创建节点
s->data=*(post+n-1); //构造根节点。
for (p=in;p<in+n;p++)//在中序中找为*ppos的位置k
if (*p==*(post+n-1))
break;
k=p-in;
s->lchild=CreateBT2(post,in,k); //构造左子树
s->rchild=CreateBT2(post+k,p+1,n-k-1);//构造右子树
return s;
}
1.1.3 线索二叉树
- 二叉链存储结构时,每个节点有两个指针域,总共有2n个指针域
- 有效指针域:n-1(根节点没指针指向)
- 空指针域:n+1
用这些空链域,指向该线性序列中的“前驱”和“后继”的指针,称作线索。
- 结构体定义:
typedef struct node
{ ElemType data; //结点数据域
int ltag,rtag; //增加的线索标记
struct node *lchild; //左孩子或线索指针
struct node *rchild; //右孩子或线索指针
} TBTNode; //线索树结点类型定义
-
结点操作
- 若结点有左子树,则lchild指向其左孩子;否则, lchild指向其直接前驱(即线索)。
- 若结点有右子树,则rchild指向其右孩子;否则, rchild指向其直接后继(即线索) 。
-
线索化类型
- 先序线索二叉树
- 后序线索二叉树
- 中序线索二叉树(在线索二叉树中再增加一个头结点 )
哈夫曼树
-
定义:给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树。
-
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
-
原则:权值越大越靠近根结点,越小越远离根结点。
-
构造过程:
- 找到权值最小的两个结点组合成一个二叉树,这个二叉树的根节点的权值为两个结点权值的和。
- 删除原来的那两个结点,把二叉树根结点放入。
- 重复以上操作直到构成一颗树。
-
结构体定义:
typedef struct
{
char data;//结点值
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, *HuffmanTree;
并查集
-
定义:查找一个元素所属的集合及合并2个元素各自专属的集合等运算
-
应用:亲戚关系、朋友圈应用。
-
结构体定义
typedef struct node
{
int dada; //结点对应人的编号
int rank; //结点秩:子树的高度,合并用
int parent; //结点对应双亲下标
}UFSTree;
- 并查集树的初始化
void MAKE_SET(UFSTree t[], int n)
{ int i;
for(i=1;i<=n;i++)
{ t[i].data=i; //数据为该人的编号
t[i].rank=0; //秩初始化为0
t[i].parent=i; //双亲初始化指向自己
}
}
- 查找一个元素所属的集合
int FIND_SET(UFSTree t[], int x)
{ if(x!=t[x].parent) //双亲不是自己
return(FIND_SET(t, t[x].parent)); //递归在双亲中找x
else
return(x); //双亲是自己,返回x
}
- 两个元素各自所属的集合的合并
void UNION(UFSTree t[], int x, int y)
{
x=FIND_SET(t, x);
y=FIND_SET(t, y);
if(t[x].rank>t[y].rank)
t[y].parent=x;
else
{
t[x].parent=y;
if(t[x].rank==t[y].rank)
t[y].rank++;
}
}
1.2 谈谈你对树的认识及学习体会
这两周学习了树的很多知识,从一开始学树学的晕头转向到现在能够掌握到一些知识,感觉自己还是有进步滴XD
一开始因为树要调用很多递归,但是上学期的递归没有很系统地学习过,PTA也没刷几题,然后这学期老师让我们在编程中感受递归,我整个人就有点慌,毕竟递归蛮抽象的。学习了之后觉得,确实还是很抽象..而且树这个东西本来就不太好调试,再加上递归,就更难找错了·_·
在经过pta的编程练习,有了一些感悟。总的来说,在这方面还是有些欠缺,最近写代码要花很多时间,感觉自己在效率上并没有很大的提升。在上机报告中,还意识到自己的不足,伪代码写的不够完善,思路还不是很清楚,还是需要多加练习。
在很多地方都可以看到树的应用,可以说树既是数据结构的重点,也是难点。只有通过对树内容的不断探索与分析,才能熟练地操作。在接下来的学习中,还是要结合之前所学的内容,进行下一步拓展。
2.阅读代码
2.1 题目及解题代码
class Solution {
public boolean isValidBST(TreeNode root) {
Stack<TreeNode> stack = new Stack();
double inorder = - Double.MAX_VALUE;
while (!stack.isEmpty() || root != null) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
// If next element in inorder traversal
// is smaller than the previous one
// that's not BST.
if (root.val <= inorder) return false;
inorder = root.val;
root = root.right;
}
return true;
}
}
2.1.1 该题的设计思路
在这里我们使用:中序遍历
左子树->结点->右子树的顺序
左子树->结点->右子树的顺序意味着,对于二叉搜索树而言,每个元素都应该比下一个元素小
这样一来算法就可以变得比较简单了:
- 计算中序遍历列表inorder
- 检查inorder中的每个元素是否小于下一个
时间复杂度:O(n)
空间复杂度:O(n)
2.1.2 该题的伪代码
建立一个栈stack并初始化;
定义inorder为中序遍历的中结点值,初始化为负最大值;
while(stack不为空or结点root不为NULL)
{
while(root不为NULL)
将root进栈,root=root.left;
root=stack.pop();
if(root值<=inorder)return false;
inorder重新赋值为root的值;
root=root.right;
}
return true;
2.1.3 运行结果
2.1.4分析该题目解题优势及难点
优势:
- 不需要保留整个inorder列表,每一步最后一个添加的元素就足以保证树是(或不是)二叉搜索树,所以可以将步骤整合并复用空间。
- 使用了栈来管理结点root,从而避免使用了递归。
难点:
- 我觉得把root改名叫node可能从代码理解上来说会更好懂一点
- 整个过程就是从左下开始不停与前一个节点比较值,往后当前节点的父节点或者右节点与前一个左节点或者父节点逐个比较,直到栈尽点绝。虽然使用栈来进行中序的方法有讲过,但是看完后,还是觉得很厉害,还是要多学习一下思路。
2.2 题目及解题代码
class Solution {
public void flatten(TreeNode root) {
if(root==null) {
return;
}
LinkedList<TreeNode> res = new LinkedList<TreeNode>();
//前序遍历整棵二叉树
dfs(root,res);
TreeNode head = res.removeFirst();
head.left = null;
//遍历链表,将链表中的TreeNode节点前后串联起来
while(res.size()>0) {
TreeNode tmp = res.removeFirst();
tmp.left = null;
head.right = tmp;
head = head.right;
}
}
//前序遍历整棵二叉树,并将遍历的结果放到数组中
void dfs(TreeNode root, List<TreeNode> res) {
if(root==null) {
return;
}
res.add(root);
dfs(root.left,res);
dfs(root.right,res);
}
}
2.2.1 该题的设计思路
展开后的链表是1->2->3->4->5->6,这个顺序就是二叉树前序遍历的顺序,我们用前序遍历的方式遍历这棵树,将结果保存到一个数组中,再把这个数组中的每个元素前后串联起来就可以了。
时间复杂度:O(N)
空间复杂度:O(N)
2.2.2 该题的伪代码
if(结点root为NULL) return;
定义数组res并初始化;
前序遍历整棵二叉树,并将遍历的结果放到数组res中;
定义TreeNode变量head并赋值为res数组的第一个,然后将res数组中的第一个值移除;
head.left=NULL;
while(res.size()>0)
{
遍历链表,将链表中的TreeNode节点前后串联起来;
}
2.2.3 运行结果
2.2.4分析该题目解题优势及难点
优势:
- 用队列作为辅助数据结构,完成二叉树的前序遍历
- 思路比较顺,其他的解法或多或少会有一些不太明白的地方
难点:
- 观察题目的样例,先序遍历中的前一个节点需要在右节点中连接先序遍历中的下一个节点,而左节点需要置空,这是在遍历过程需要思考的问题
2.3 题目及解题代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
vector<int> res;
if(root==NULL){
return res;
}
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int n=q.size();
for(int i=1;i<=n;i++){
TreeNode* node =q.front();
q.pop();
if(i==n){
res.push_back(node->val);
}
if(node->left){
q.push(node->left);
}
if(node->right){
q.push(node->right);
}
}
}
return res;
}
};
2.3.1 该题的设计思路
大概思路:题目需要遍历出一颗二叉树右视图结点元素,那么意味着在经过层次遍历后,每次将最后一个结点取出即可
我们对每一层都从左到右访问。因此,通过只保留每个深度最后访问的结点,我们就可以在遍历完整棵树后得到每个深度最右的结点
时间复杂度:O(n)
空间复杂度:O(n)
2.3.2 该题的伪代码
vector<int> rightSideView(TreeNode* root)
{
创建一维数组res接收结果值;
if (根节点为空)
return ans;
定义一个队列q存储结点;
while (队列不为空)
队列长度赋值int n = q.size();
for(int i =1 to n)
TreeNode* node =q.front();
q.pop();
if(i==n)
res.push_back(node->val);
if (左孩子不为空)
q.push(node->left);
if (右孩子不为空)
q.push(node->right);
返回结果集res
}
2.3.3 运行结果
2.3.4分析该题目解题优势及难点
优势:
- 将我们所学的层次遍历运用到这题当中,使代码实现起来更加的方便,简化了思路。
- 通过执行将左结点排在右结点之前的广度优先搜索,对每一层都从左到右访问。因此,通过只保留每个深度最后访问的结点,就可以在遍历完整棵树后得到每个深度最右的结点。
难点:
- 本题需要定义接收目标结点指针node,进入循环给它赋值为每层第一个结点,然后再把第一个结点pop出去,再赋值为当前队列q的第一个,这样node每次取出的就是一层中最后一个结点了,最后放入数组即可完成右视图遍历
2.4 题目及解题代码
class Solution {
public int numTrees(int n) {
if(n<=0) return 1;
int [] dp_Catalan=new int[n+1];
dp_Catalan[0]=1;
dp_Catalan[1]=1;
for(int i=2;i<=n;i++){
for(int k=0;k<=i-1;k++){
dp_Catalan[i]+=dp_Catalan[k]*dp_Catalan[i-k-1];
}
}
return dp_Catalan[n];
}
}
2.4.1 该题的设计思路
-
思路:动态规划
-
假设n个节点存在二叉排序树的个数是G(n),令f(i)为以i为根的二叉搜索树的个数,则G(n) = f(1) + f(2) + f(3) + f(4) + ... + f(n)G(n)=f(1)+f(2)+f(3)+f(4)+...+f(n)
-
当i为根节点时,其左子树节点个数为i-1个,右子树节点为n-i,则f(i) = G(i-1)*G(n-i)f(i)=G(i−1)∗G(n−i)
-
综合两个公式可以得到 卡特兰数 公式G(n) = G(0)×G(n-1)+G(1)×(n-2)+...+G(n-1)×G(0)
-
时间复杂度:O(n²) 上述算法的主要计算开销在于包含 G[i] 的语句。因此,时间复杂度为这些语句的执行次数
空间复杂度:O(n) 上述算法的空间复杂度主要是存储所有的中间结果
2.4.2 该题的伪代码
class Solution {
public int numTrees(int n) {
if(为空树) return 1;//空树也是二叉搜索树
int [] dp_Catalan=new int[n+1];//dp[i]就表示i个节点的情况个数
初始化卡特兰数前两项;
for(int i=2;i<=n;i++)
for(int k=0;k<=i-1;k++)
运用卡特兰公式进行求解;
return dp_Catalan[n];
}
}
2.4.3 运行结果
2.4.4分析该题目解题优势及难点
优势:
- 运用数学知识点,能够很好地解决代码冗长的问题
- 动态规划的思路很巧妙,代码实现起来更方便了
难点:
- 这个数学公式嘛,不太好理解,也没接触过,然后题目用回溯的方法容易运行超时,所以还是要多多积累
- G[0]=1这个点不是很好理解,因为涉及到乘法,如果G[0]=0的话,乘完就为0了,这是不对的