二叉树知识的总结
文章目录
如何理解树:
学有所思,学有所指。
随着所处理的问题不再是只用一条简单的链表就能处理的时候,我们迎来了树。
通过对比我们发现:链表就是前一个指向后一个,并且指向是唯一的。而树就是前一个指向后多个,就只是指向的位置个数不同,没什么区别。那么,为什么叫它是树呢?因为长得像啊,以至于后面的许多名词,都是因为长得像,为了方便表达而诞生的 代名词。
首先介绍的是:二叉树。其实三个部分就是一个二叉树了:左子树(指针),根(数据),右子树(指针)。然后,通过循环的形式,不断的接上其他的树,组成一个大树。而其中的各种花里胡哨的名词:什么叶子结点啊,根结点啊,我们都不能否认单独拿一个结点出来,它依然可以是一个树。只是根结点的两边指针指向的不是空,叶子节点指向的是空而已。而这个 ”空“ 我们为什么要对它有偏见呢?它也不过只是一个空间的 代名词 而已,和 “树” 这个词在本质没什么区别。
二叉树的实践编写部分:
二叉树的前中后序遍历(也叫深度优先)
即便我们理解了树的组成,但是对于编译来说,我们的输入数据或者输出数据都具有”固定顺序“:从左到右。那么,如何让我们的树和计算机产生交流的“桥梁”呢。于是便诞生了三种遍历方式。就像我说的:它们都是一些有具体指向的代名词。
前序遍历,就是根(数据)先被输出,然后再找到左子树,把左子树拿出来,输出根,直到遇到空,找右子树把他拿出来,输出根,直到遇到空。中序遍历就是把根放在中间输出,左->根->右,后序就是:把根放在最后:左->右->根。
注意:要把每一个树(或者称之为结点)都进行对应的顺序输出,直到遇到空,这也是深度优先得名字来源。
参考代码:
viod qianxu(Shu* shu){ //前序
if(shu == NULL)
return;
printf("%d",shu->data);
qianxu(shu->left);
qianxu(shu->right);
}
viod zhongxu(Shu* shu){ //中序
if(shu == NULL)
return;
zhongxu(shu->left);
printf("%d",shu->data);
zhongxu(shu->right);
}
viod houxu(Shu* shu){ //后序
if(shu == NULL)
return;
houxu(shu->left);
houxu(shu->right);
printf("%d",shu->data);
}
其实也没什么区别,就是依次从左到右遍历,把输出数据放在了不同的位置。
由两个序列构建唯一二叉树
在介绍了三个遍历后,再来写一下由两个序列构建唯一二叉树吧。
首先给结论:
- 由前序和中序可以构建
- 由后序和中序可以构建
为什么中序很重要呢?不着急,我们先来分析一下三个序列的特点:
1.前序:根节点在首位
2.中序:根节点分开了左子树和右子树
3.后序:根节点在末尾
相信你已经猜出构建的逻辑了。
是的,我们通过根节点不断的把大树分为一个个左右小树,然后依次递归便实现了创建。
参考代码(主要是理解逻辑)
TNode* InPreToTree(char *pa, char *ia, int p1, int p2, int i1, int i2)
{ //pa:前序字符,ia:中序字符,p1:前序首下标p2:前序尾下标 i1,i2同前
if(p1>p2){ //没有前序就接空
return NULL;
}
char pa_root_val = pa[p1]; //找到根结点,每三个结点构成一个树
TNode* root = (TNode *)malloc(sizeof(TNode));
root->data = pa_root_val;//维护所谓的根结点,其实叶结点的子结点就是 空
int ia_root = i1; //记录索引位置
while(ia[ia_root] != pa_root_val ){
ia_root++; //中序分割左树
}
int ia_left_size = ia_root-i1; //不算根结点,取左树长度
root->left = InPreToTree(pa,ia,p1+1,p1+ia_left_size,i1,,ia_root-1);
//p1+1是维护前序根结点,p1+size是得到左树尾部索引。i1不变,ia_root-1得到左子树的尾下标
root->right = InPreToTree(pa,ia,p1+ia_left_size+1,p2,ia_root+1,i2);
//p1+size得到左树,+1得到右树根,ia_root+1得到中序的右树。
return root;
}
其实后序和它也差不多,就不演示了。
广度优先(二叉树叫层次遍历)
还是由名得意,相对于深度优先的每一次都要找到底部(null)才罢休,广度优先则是注重广度,一层一层的依次遍历。具体的实现逻辑:输出当前树的data,把它的left和right进队列,然后不断让队列里的数据出来,(出来的树也会继续存left和right),直到队为空。
参考伪代码(主要是理解逻辑):
viod shengdu(Shu* shu ){
Queue queue; //queue:队列(这个队列就自己写哈)
queue.add(shu); //入队,
while( !queue.isEmpty )//队列不为空
{
Shu root = queue.remove();//出队
printf("%d",root.data);
if(root.left !=NULL )
queue.add(root.left);
if(root.right != NULL )
queue.add(root.right);
}
}
二叉树的理论做题部分
五大性质及其理解:
- 性质一:在二叉树的i层上至多有2^ (k-1)个节点(i>=1)至少有1个
理解:等比数列的值
-
性质二:深度为k的二叉树至多有2^k-1个节点,至少为k个
理解:等比数列求和,K个就是全左子树。
- 性质三:对任何一棵二叉树T,终端节点数为n0,记:度为2的结点为n2,度为0的结点为n0 则n0=n2+1
理解:首先对于满树:我们把最外层分开,n2其实就是全部结点:2k-1,因为我们剥掉了最外层,于是记为:2i-1(i = k-1).而n0就是叶结点:2^ (k-1)也就是:2^i。
然后,由一般推特殊:如果我们少一个右叶子结点,也就是:n0-1,我们发现:n2也会-1。于是得到了普遍的结果。
-
性质四:具有n个节点的完全二叉树的深度为[log2n]+1向下取整
理解:还是等比数列,由于是完全二叉树,那么我们把最外层扒掉,也就是公式的”+1“,然后满树:m = 2^k-1,取对数:k = log2 (m+1),考虑到实际 m+1<=n,多的那点数值构不成下一层,于是[log2 n]向下取整。
-
性质五:如果有一颗有n个节点的完全二叉树的节点按层次序编号,对任一层的节点i(1<=i<=n)有
1.如果i=1,则节点是二叉树的根,无双亲,如果i>1,则其双亲节点为[i/2],向下取整
理解:由于完全二叉树,做序号,每一个序号其实加的是两个序号节点。
2.如果2i>n那么节点i没有左孩子,否则其左孩子为2i
理解:依然是一个节点接两个的性质,>n就是代表没有这么多。
3.如果2i+1>n那么节点没有右孩子,否则右孩子为2i+1
理解:同上,+1就是从左到右加一个序号。
二叉排序树
构建:
简单说:就是给每一个点都标了权值,构建的时候呢把这个新的结点与根结点比较:如果这个结点小于根节点,那就准备放在他的左边(反之右边),如果它的左边有结点,那就再比较,再放,直到遇到null能接上为止。这样就构成了二叉排序树。
作用:方便进行二分查找。
平衡二叉树
简单的说,在二叉排序树的基础上加一个限制:每一个结点的左右子树的深度之差不能超过1(也就是-1,0,1这三个数值)。
也就是说,依旧先按照二叉排序树的方法构建,每插入一个树就进行一个判断:看一下有没有破坏平衡了啊。如果有的话,那就调整一下:
参考代码:
typedef struct Tree {
int value; //自己的值
int depth; //存自己的深度
int ret; //判断目前平衡与否
struct Tree *left;
struct Tree *right;
} Tree;
//维护depth和ret:
void tree_adjust(Tree* root){
if(root->left == NUll){//只会有一边是空的
root->ret = 1;
}else if(root->right == NUll){
root->ret = -1;
}else{
root->ret = root->left->depth - root->right->depth;
}
if(ret>0){
root->depth = root->left->depth+1;//维护深度
}else{
...
}
}
//调整左子树多了:
Tree* left_adjust(Tree* root){
Tree* A = root->left;
Tree* B = A->right ; //就算是null也无所谓
A->right = root;
root->left = B;
//维护数据:
tree_adjust(root);
tree_adjust(A);
//至于有没有可能维护后还需要调整呢?
//这是不需要的,可以依据平衡二叉树的性质自己想一想
return A;
}//由于之前就是排序树,这样调整依旧满足排序树。
Tree* In_Tree(Tree* root,Tree* node){
if(node->value<root->value){
if(root->left == null){
root->left = node;
root->depth +=1;
root->ret+=1 //左-右,那就是+1
}else{
In_Tree(root->left,node)
}
}else{
...//右边就不写了
}
//维护一下数据:
tree_adjust(root);
if(root->ret >= 2){ //其实==2就够了
return left_adjust(root);
}else{
...
}
return root;
}
至于它的作用嘛,是优化二分查找。
代码好像有点多了,其实也只可以截取一部分函数去看的。
对一些题目描述的思考
-
度为2的结点为n2,度为0的结点为n0
可以这样理解:那么是不是意味着有n2*2个结点?如此,对于一个度为4的树就有:
n = n1+n2 * 2+n3 * 3+n4 * 4 这样就在做题的时候多一个等式可以用了。
-
有的题目问的是叶子节点树(n0)不要看错了哈。
-
有的题目需要我们把树的左右子树换位置,然后进行对应的操作。那我们其实可以从右到左去输出这个树,这样就从输出顺序的角度完成了结果上的位置交换。
-
以及,每一个所谓的知识点,基本都是在原有的东西的基础上加了点限制,或者说加了的功能,就延申出了所谓的新的问题,新的知识,新的规律。
最后的一些自言自语
好了,关于二叉树的总结就到这里了哈。
不过,我们的征程还未结束。