二叉树的总结

本文总结了二叉树的基本概念,包括二叉树的构成、前中后序遍历,以及深度优先和广度优先搜索。还探讨了二叉树的五大性质,如节点数量、完全二叉树的构造与性质五,以及平衡二叉树的概念。最后提到了哈夫曼树和图的相关内容,展示了二叉树理论在实际问题中的应用。
摘要由CSDN通过智能技术生成

二叉树知识的总结

如何理解树:

学有所思,学有所指。

随着所处理的问题不再是只用一条简单的链表就能处理的时候,我们迎来了树。

通过对比我们发现:链表就是前一个指向后一个,并且指向是唯一的。而树就是前一个指向后多个,就只是指向的位置个数不同,没什么区别。那么,为什么叫它是树呢?因为长得像啊,以至于后面的许多名词,都是因为长得像,为了方便表达而诞生的 代名词。

首先介绍的是:二叉树。其实三个部分就是一个二叉树了:左子树(指针),根(数据),右子树(指针)。然后,通过循环的形式,不断的接上其他的树,组成一个大树。而其中的各种花里胡哨的名词:什么叶子结点啊,根结点啊,我们都不能否认单独拿一个结点出来,它依然可以是一个树。只是根结点的两边指针指向的不是空,叶子节点指向的是空而已。而这个 ”空“ 我们为什么要对它有偏见呢?它也不过只是一个空间的 代名词 而已,和 “树” 这个词在本质没什么区别。

在这里插入图片描述

二叉树的实践编写部分:

二叉树的前中后序遍历(也叫深度优先)

即便我们理解了树的组成,但是对于编译来说,我们的输入数据或者输出数据都具有”固定顺序“:从左到右。那么,如何让我们的树和计算机产生交流的“桥梁”呢。于是便诞生了三种遍历方式。就像我说的:它们都是一些有具体指向的代名词。

前序遍历,就是根(数据)先被输出,然后再找到左子树,把左子树拿出来,输出根,直到遇到空,找右子树把他拿出来,输出根,直到遇到空。中序遍历就是把根放在中间输出,左->根->右,后序就是:把根放在最后:左->右->根。

注意:要把每一个树(或者称之为结点)都进行对应的顺序输出,直到遇到空,这也是深度优先得名字来源。

参考代码:

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)不要看错了哈。

  • 有的题目需要我们把树的左右子树换位置,然后进行对应的操作。那我们其实可以从右到左去输出这个树,这样就从输出顺序的角度完成了结果上的位置交换。

  • 以及,每一个所谓的知识点,基本都是在原有的东西的基础上加了点限制,或者说加了的功能,就延申出了所谓的新的问题,新的知识,新的规律。

最后的一些自言自语

好了,关于二叉树的总结就到这里了哈。

不过,我们的征程还未结束。

这里留一个关于哈夫曼树的网址吧,这个up主讲的很清楚呢。
以及:下一篇图的总结

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值