王道数据结构笔记05—树(c语言)

1.树的基本概念

树:从树根生长,逐渐分支
在这里插入图片描述
非空树的特征:
1.有且仅有一个根节点
2.没有后继的结点成为“叶子结点”
3.有后继的结点成为“分支结点”
4.除了根节点外,任何一个结点都有且仅有一个前驱
5.每个结点可以有0个或多个后继
除根节点外的结点可以分为多个子树
在这里插入图片描述
几个术语:

1.祖先结点:从一个结点出发往上走,直到回到根节点,所经过的所有结点
2.子孙结点:从一个结点出发,其分支的全部结点
3.双亲结点:一个结点的直接前驱
4.孩子结点:一个结点的直接后继
5.兄弟结点:一个结点的所有直接后继
6.结点的度:一个结点的分支数
7.树的度:各结点的度的最大值
8.有序树:树中结点的各子树从左至右是有次序的,不可互换
9.无序树:树中结点的各子树从左至右是无次序的,可以互换
10.森林:由n颗互不相交的树的集合
11.m叉树:每个结点的分支数最多不超过m的树

树的常考性质:

1.结点数=总度数+1
2.度为m的树第i层至多有mi-1个结点
3.高度为h的m叉树至少有h个结点
4.高度为h,度为m的树至少有h+m-1个结点
5.高度为h的m叉树至多有:mh-1/m-1个结点(等比数列求和)

2.二叉树

2.1二叉树的基本概念

二叉树的特点:
1.每个结点至多只有两颗子树
2.左右子树不能颠倒(二叉树是有序树)

在这里插入图片描述

满二叉树
一颗高度为h,含有2h-1个结点的二叉树
在这里插入图片描述
完全二叉树
当且仅当其每个结点都与高度为h的满二叉树中编号1~n的结点一一对应时,成为完全二叉树
在这里插入图片描述
二叉排序树
左子树上所有结点的关键字均小于根节点的关键字,右子树上所有结点的关键字均大于根节点的关键字的二叉树。
在这里插入图片描述
平衡二叉树
树上任一结点的左子树和右子树的深度之差不超过1
在这里插入图片描述
二叉树的常考性质
1.设非空二叉树中度为0,1和2的结点个数分别为n0、n1、n2,则n0=n2+1(叶子结点比二分支结点多一个)

设树中总结点数为n
n=n0+n1+n2
n=n1+2n2+1(数的结点数=总度数+1)

2.具有n个(n>0)结点的完全二叉树的高度h为[log2(n+1)]或[log2n]+1

左式
高度为h的满二叉树共有2h-1个结点
高度为h-1的满二叉树共有2h-1-1个结点
2h-1<h<2h-1-1
右式
高度为h-1的满二叉树共有2h-1-1个结点,高为h的完全二叉树至少有2h-1个结点,至多有2h-1个结点
2h-1<n<2h-1

3.对于完全二叉树,可以由结点数n推出度为0,1和2的结点个数为n0,n1和n2

完全二叉树最多只有一个度为1的结点:
n1=0或1
n0=n2+1
若完全二叉树有2k个结点,则必有:
n1=1,n0=k,n2=k-1
若完全二叉树有2k-1个结点,则必有:
n1=0,n0=k,n2=k-1

2.2二叉树的存储结构

顺序存储

#define MaxSize 100
struct TreeNode{
    ElemType value;//结点中的数据元素
    bool isEmpty;//结点是否为空
};
int main (){
    //定义一个长度为MaxSize的数组t,按照从上至下,从左至右的顺序依次存储完二叉树中的各个结点
    TreeNode t[MaxSize];
    for(int i=0;i<MaxSize;i++)
        t[i].isEmpty=true;
}

我们可以从t[1]开始存储数据,这样正好和二叉树的编号对上
在这里插入图片描述
若用上文的数组存储完全二叉树,则有一下结果基本操作
1.i的左孩子 ——2i
2.i的右孩子 ——2i+1
3.i的父节点 ——[i/2]
4.i所在的层次 ——[log2(n+1)]或[log2n]+1
若存储的如下图所示的非完全二叉树
在这里插入图片描述

二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来,所以在存储非完全二叉树时会浪费大量空间,只适合存储完全二叉树

在这里插入图片描述

链式存储

typedef struct BiTNode{
    ElemType data;           //数据元素
    struct BiTNode* lchild;//左孩子
    struct BiTNode* rchild;//右孩子
}BiTNode,*BiTree;

在这里插入图片描述

链式二叉树的初始化

int main (){
   //定义一颗空树
   BiTree root = NULL;
   //插入根节点
   root = (BiTree)malloc(sizeof(BiTNode));
   root->data=1;
   root->lchild=NULL;
   root->rchild=NULL;
   //插入新节点
   BiTNode* p = (BiTNode*)malloc(sizeof(BiTNode));
   p->data=2;
   p->lchild=NULL;
   p->rchild=NULL;
   root->lchild=p;
}

在这里插入图片描述

对于上述链式存储,找到左右孩子较为容易,但是找到父节点只能遍历,所以如果要频繁地查找父节点,可以在结构体指针中加一个parent指针指向父结点。

2.3二叉树的遍历

2.3.1二叉树的先中后序遍历

三种遍历的结点访问顺序:

先序遍历:根左右(NLR)
中序遍历:左根右(LNR)
后序遍历:左右根(LRN)

举一个栗子

下文的代码都基于二叉树的链式存储,定义上文有
先序遍历

1.若二叉树为空,则什么也补做
2.若二叉树非空:
步骤一:访问根节点
步骤二:先序遍历左子树
步骤三:先序遍历右子树

void PreOrder(BiTree T){
    if(T!=NULL){
        visit(T);//访问结点的函数,可以在里面定义一系列访问操作等,如打印数据
        PreOrder(T->lchild);
        PreOrder(T->rchild);
    }
}

中序遍历

1.若二叉树为空,则什么也不做
2.若二叉树非空:
步骤一:中序遍历左子树
步骤二:访问根节点
步骤三:中序遍历右子树

void InOrder(BiTree T){
    if(T!=NULL){
        InOrder(T->lchild); //递归遍历左子树
        visit(T);           //访问根节点
        InOrder(T->rchild); //递归遍历右子树
    }
}

后序遍历

1.若二叉树为空,则说明也不做
2.若二叉树非空:
步骤一:后序遍历左子树
步骤二:后序遍历右子树
步骤三:访问根节点

void PostOrder(BiTree T){
    if(T!=NULL){
        PostOrder(T->lchild);//递归遍历左子树
        PostOrder(T->rchild);//递归遍历右子树
        visit(T);            //访问根节点
    }
}

2.3.2二叉树的层次遍历

在这里插入图片描述

层次遍历:即一层一层的访问二叉树
思路:
1.初始化一个辅助队列
2.根节点入队
3.若队列非空,则队头元素出队,访问该结点,并将其左、右孩子插入队尾
4.重复3直至队列为空

关于队列的操作的具体代码实现可以参考这篇笔记
王道数据结构笔记03—栈与队列(C语言)

typedef struct LinkNode{
    BiTNode* data;
    struct LinkNode* next;
}LinkNode;

typedef struct{
    LinkNode *front;
    LinkNode *rear;//队头队尾
}LinkQueue;
void LevelOrder(BiTree T){
    LinkQueue Q;
    InitQueue(Q);//初始化一个辅助队列
    BiTree p;
    EnQueue(Q,T);//将根结点入队
    while(isEmpty(Q)){//队列不空则循环
        DeQueue(Q,p);//队头结点出队
        visit(p);//访问出队结点
        if(p->lchild!=NULL)
            EnQueue(Q,p->lchild);//左孩子入队
         if(p->rchild!=NULL)
            EnQueue(Q,p->rchild);//右孩子入队
    }
}

2.3.3由遍历序列构造二叉树

对于同一个遍历序列,可以对应多种二叉树的形态
以前序遍历为例,其余两种也类似
在这里插入图片描述
若要确定唯一的一颗二叉树,我们需要以下的一种:

前序+中序遍历序列
后序+中序遍历序列
层序+中序遍历序列

前序+中序遍历序列
在这里插入图片描述
此时,若给定两个遍历序列:
前序遍历序列:A D B C E
中序遍历序列:B D C A E
由前序遍历的顺序,我们可以确定,A为根节点,由中序遍历的顺序,我们可以确定A前的BDC是A的左子树,A后的E为A的右子树。同理,我们可以确定D是左子树的根节点,B是左孩子,C是右孩子。可得如下图所示的二叉树。
在这里插入图片描述
后序+中序遍历序列
在这里插入图片描述
此时,若给定两个序列:
后序遍历序列:E F A H C I G B D
中序遍历序列:E A F D H C B G I
由后序序列的顺序,可知D为根节点,由中序遍历的顺序,可知EAF为D的左子树,HCBGI为D的右子树,同理可以判断左右子树的结构,如下图所示
在这里插入图片描述
层序+中序遍历序列

在这里插入图片描述
此时,若给定两个序列:
层序遍历序列:D A B E F C G H I
中序遍历序列:E A F D H C B G I
由层序遍历的顺序,可知D是根节点,由中序遍历的顺序,可知,EAF是D的左子树,HCBGI是D的右子树,又由层序遍历的顺序可知,A为左子树的根节点,B为右子树的根节点,又由中序遍历的顺序可知,E为A的左孩子,F为A的右孩子,HC为B的左孩子,GI为B的右孩子,同理推出余下的部分。
在这里插入图片描述

2.4线索二叉树

2.4.1线索二叉树的作用

在谈作用前,我们先看一下中序遍历的缺点
对于如图的二叉树,我们能否通过G结点中序遍历呢?
在这里插入图片描述

答案显然是否定的,因为G结点中只保存了其左右孩子的指针,所以要遍历只能从根节点开始,并且,给定一个结点,要找出其前驱,我们也只能通过从根节点开始遍历。

中序线索二叉树

为了构造线索二叉树,我们可以把某些没有左或右孩子的结点的左右孩子指针利用起来,来指向其前驱或者后继

在这里插入图片描述

此时,我们若还要找G结点的后继,就会变得很容易了,(对于左右指针指向的是左右孩子,非前驱后继的结点,下文再讨论)
线索二叉树的存储结构

typedef struct ThreadNode{
    int data;
    struct ThreadNode *lchild;
    struct ThreadNode *rchild;
    //左右孩子标志
    int ltag;
    int rtag;
}ThreadNode,*ThreadTree;

tag为1时,说明左后孩子为前驱与后继,当tag为0时,说明其是左右孩子。下面用一张图来表示其的存储结构。
在这里插入图片描述
先序线索二叉树
先序与后续线索二叉树与中序其实是类似的,只是遍历顺序的不同。直接给出图例。
在这里插入图片描述

存储视角
在这里插入图片描述

后续线索二叉树
在这里插入图片描述

存储结构
在这里插入图片描述

2.4.2二叉树的线索化

在2.4.1中,我们了解了什么是线索二叉树,在这一节中,主要是线索二叉树的代码实现。

用遍历的方法找到中序前驱

//辅助全局变量,用于查找结点p的前驱
BiTNode *p;//p指向目标结点
BiTNode *pre=NULL;//指向当前访问结点的前驱
BiTNode *final=NULL;//用于记录最终的结果

//中序遍历
void InOrder(BiTree T){
    if(T!=NULL){
        InOrder(T->lchild); //递归遍历左子树
        visit(T);           //访问根节点
        InOrder(T->rchild); //递归遍历右子树
    }
}

//访问结点q
void visit(BiTNode *q){
    if(q==p)//当前访问的结点刚好是p
        final =pre;//找到p的前驱
    else
        pre=q;//pre指向当前访问的结点
}

中序线索化

//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;

//中序遍历二叉树,边遍历边线索化
void visit(ThreadNode* q){
    if(q->lchild==NULL){//左子树为空,建立前驱线索
        q->lchild=pre;
        q->ltag=1;
    }
    if(pre!=NULL&&pre->rchild==NULL){
        pre->rchild=q;//建立前驱结点的后继线索
        pre->rtag=1;
    }

    pre=q;
}
void InThread(ThreadTree T){
    if(T!=NULL){
        InThread(T->lchild);//中序遍历左子树
        visit(T);//访问根节点
        InThread(T->rchild);//中序遍历右子树
    }
}

//中序线索化二叉树
void CreatInThread(ThreadTree T){
    pre=NULL;
    if(T!=NULL){//非空二叉树才能线索化
        InThread(T);//中序线索化二叉树
        if(pre->rchild==NULL)
            pre->rtag=1;//处理遍历的最后一个结点的右孩子
    }
}

最后得到的二叉树如下图
在这里插入图片描述
先序线索化

先序线索化的visit函数和全局变量pre与中序线索化一致。只是改变了访问结点的顺序而已,后文的后序线索化同理。

//先序遍历二叉树,边遍历边线索化
void PreThread (ThreadTree T){
    if(T!=NULL){
        visit(T);
        if(T->ltag==0)//因为先序遍历会先访问根节点,在去访问左子树,为了避免重复访问已经被设为前驱线索的结点,这里要加一个限制
            PreThread(T->lchild);
        PreThread(T->rchild);
    }
}

//先序线索化二叉树
void CreatPreThread(ThreadTree T){
    pre=NULL;
    if(T!=NULL){//非空二叉树才能线索化
        PreThread(T);//先序线索化二叉树
        if(pre->rchild==NULL)
            pre->rtag=1;//处理遍历的最后一个结点
    }
}

后序线索化

//后序遍历二叉树,边遍历边线索化
void PostThread(ThreadTree T){
    if(T!=NULL){
        PostThread(T->lchild);//后序遍历左子树
        PostThread(T->rchild);//后序遍历右子树
        visit(T);//访问根节点
    }
}

//后序线索化二叉树
void CreatPoatThread(ThreadTree T){
    pre=NULL;
    if(T!=NULL){
        PostThread(T);
        if(pre->rchild==NULL)
            pre->rtag=1;
    }
}

2.4.3线索二叉树找前驱与后继

中序线索二叉树找中序后继
在这里插入图片描述

//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode* FirstNode(ThreadNode *p){
    //循环找最左下的结点
    while(p->lchild==0)
        p=p->lchild;
    return p;
}

//在中序线索二叉树中找到结点p的后继结点
ThreadNode *NextNode(ThreadNode *p){
    //右子树中最左下结点
    if(p->rtag==0)
        return FirstNode(p->rchild);
    else
        return p->rchild;
}

//对中序线索二叉树进行中序遍历
void InOrder(ThreadNode *T){
    for(ThreadNode *p=FirstNode(T);p!=NULL;p=NextNode(p))
        visit(p);
}

中序线索二叉树找中序前驱
在这里插入图片描述

//找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *LastNode(ThreadNode *p){
    //循环找到最右下结点
    while(p->rtag==0)
        p=p->rchild;
    return p;
}
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *PreNode(ThreadNode *p){
    //左子树中最右下结点
    if(p->lchild==0)
        return LastNode(p->lchild);
    else
        return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){
    for(ThreadNode *p=LastNode(T);p!=NULL;p=PreNode(p))
        visit(p);
}

先序线索二叉树找先序后继
在这里插入图片描述
先序线索二叉树找先序前驱

在这里插入图片描述

所以,传统的二叉树我们无法快速的找到先序前驱,但是如果我们能找到p的父结点,那么有以下几种情况

1.能找到p的父结点,且p是左孩子
则:p的父结点即为其前驱
2.能找到p的父结点,且p是右孩子,其左兄弟为空
则:p的父结点即为其前驱
3.能找到p的父结点,且p是右孩子,其左兄弟非空
则:p的前驱为左兄弟子树中最后一个被先序遍历的结点
4.若p没有父结点
则:p没有先序前驱
后序线索二叉树找后续前驱
在这里插入图片描述
后序线索二叉树找后序后继
在这里插入图片描述

所以,传统的二叉树无法快速找到后序后继,若我们能知道p的父结点,就有以下的几种情况

1.若能找到p的父结点,且p是右孩子
则:p的后继是其父结点
2.若能找到p的父结点,且p是左孩子,其右兄弟为空
则:p的后继是其父结点
3.若能找到p的父结点,且p是左孩子,其右兄弟非空
则:p的的后继是右兄弟子树中第一个被后序遍历的结点
4.若p没有父结点
则:p没有后序后继

3.树的存储结构

在此之前,我们讨论的都是二叉树,先回忆一下树的逻辑结构,树有且仅有一个根节点,并且每个结点都可以有m(m>0)颗子树

双亲表示法(顺序存储)
双亲表示法:每个结点中保存指向双亲的指针
在这里插入图片描述

#define Max_Tree_Size 100
typedef struct {//树的结点定义
    ElemType data;//数据元素
    int parent;//双亲位置域
}PTNode;
typedef struct{//树的类型定义
    PTNode nodes[Max_Tree_Size];//双亲表示
    int n;//结点数
}PTree;

插入一个新元素:
直接新增元素并且记录其父结点即可
找到孩子结点:
只能从头遍历
删除一个元素:
若删除的是叶子结点,可以直接将其parent设为-1或者用数组后面的结点去覆盖他,注意若不是叶子结点,则其后面的子树也要一起删除(很麻烦)
孩子表示法(顺序——链式存储)

在这里插入图片描述

#define Max_Tree_Size 100
struct CTNode{
    int child;//孩子结点在数组中的位置
    struct CTNode *next;//下一个孩子
};
typedef struct{
    ElemType data;
    struct CTNode *firstchild;//第一个孩子
}CTBox;
typedef struct {
    CTBox nodes[Max_Tree_Size];
    int n,t;//结点数和根的位置
}CTree;

孩子兄弟表示法(链式存储)
用该表示法,可以实现树与二叉树的转换
每个结点存储第一个孩子和右兄弟指针

typedef struct CSNode{
    int data;
    struct CSNode *firstchild,*nextsibling;//第一个孩子和右兄弟指针
}CSNode,*CSTree;

树转化为二叉树
在这里插入图片描述
二叉树转化为树
在这里插入图片描述
森林转换为二叉树

先把森林中的左右树转换为二叉树,再把每颗二叉树的根节点看为兄弟关系相连。

在这里插入图片描述
二叉树转换为森林
在这里插入图片描述

4.树与森林的遍历

4.1树的遍历

先根遍历
在这里插入图片描述
后根遍历
在这里插入图片描述
层序遍历(广度优先)

1.新建一个辅助队列
2.若队列非空,则根节点入队
3.若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
4.重复3直到队列为空

4.2森林的遍历

先序遍历

若森林非空,则按如下规则进行遍历:
1.访问森林中第一棵树的根节点
2.先序遍历第一棵树中根节点的子树森林
3.先序遍历除去第一颗树之后剩余的树构成的森林

在这里插入图片描述
在这里插入图片描述

中序遍历

若森林非空,则按如下规则进行遍历
1.中序遍历森林中第一棵树的根节点的子树森林
2.访问第一棵树的根节点
3.中序遍历除去第一棵树之后剩余的树构成的森林

在这里插入图片描述
在这里插入图片描述

5.几种特殊的树

5.1 二叉排序树

5.1.1 二叉排序树(BST)的定义

左子树结点值<根结点值<右子树结点值

在这里插入图片描述
所以,进行中序遍历,我们可以得到一个递增的有序序列
代码定义:

typedef struct BSTNode{
    int key;
    struct BSTNode *lchild;
    struct BSTNode *rchild;
}BSTNode,*BSTree;

5.1.2二叉排序树的基本操作

二叉排序树的查找

     BSTNode *BST_Search(BSTree T,int key){
    while(T!=NULL&&key!=T->key){
        //小于,则在左子树上找,大于,则在右子树上找
        if(key<T->key)
            T=T->lchild;
        else
            T=T->rchild;
    }
    return T;
}

二叉排序树的插入

int BST_Insert (BSTree &T,int k){
    if(T==NULL){
        T=(BSTree)malloc(sizeof(BSTNode));
        T->key=k;
        T->lchild=NULL;
        T->rchild=NULL;
        return 1;//返回1,插入成功
    }
    else if(k==T->key)
        return 0;//树中存在相同的结点,插入失败
    else if(k<T->key)
        return BST_Insert(T->lchild,k);//插入到左子树
    else
        return BST_Insert(T->rchild,k);//插入到右子树
}

在这里插入图片描述
二叉排序树的构造

void Creat_BST(BSTree &T,int str[],int n){
    T=NULL;//初始时T为空树
    int i=0;
    while(i<n){
        BST_Insert(T,str[i]);
        i++;
    }
}

二叉排序树的删除

先搜索找到目标结点:
1.若被删除的是叶子结点,则直接删除,不会破坏二叉排序树的性质
2.若被删除的结点只有一颗子树,则让其子树成为父结点的子树,代替其位置
3.若被删除的结点有左右子树,则令其右子树最左下结点(直接后继)或左子树最右下结点(直接前驱)替代其位置,再从二叉排序树中删除这个结点

查找效率分析
查找长度:在查找运算中,需要对比关键字的次数成为查找长度,反映了查找操作时间复杂度。
平均查找长度ASL
在这里插入图片描述

在这里插入图片描述

5.2平衡二叉树(AVL)

平衡二叉树:树上任一结点的左子树和右子树的高度之差不超过1
结点的平衡因子:左子树高-右子树高
座椅各个结点的平衡因子只可能是-1,0,1

代码定义

typedef struct AVLNode{
    int key;//数据
    int balance;//平衡因子
    struct AVLNode *lchild;
    struct AVLNode *rchild;
}AVLNode,*AVLTree;

5.2.1平衡二叉树的插入

对于平衡二叉树,重点在于插入后如何保持平衡。做题技巧:找到最小不平衡二叉树对其进行旋转

左孩子:右旋
右孩子:左旋

LL:在A的左孩子的左子树中插入导致不平衡
在这里插入图片描述
代码思路:
在这里插入图片描述

RR:在A的右孩子的右子树中插入导致不平衡
在这里插入图片描述
在这里插入图片描述
LR:在A的左孩子的右子树中插入导致不平衡
在这里插入图片描述
在这里插入图片描述
RL:在A的右孩子的左子树中插入导致不平衡
在这里插入图片描述
在这里插入图片描述
查找效率分析:
假设以nh表示深度为h的平衡树中含有的最少结点数
则有n0=0,n1=1,n2=2,且nh=nh-1+nh-1+1
含有n个结点的平衡二叉树的最大深度为log2n,平均查找长度为O(log2n)

5.3哈夫曼树

5.3.1带权路径长度(WPL)

结点的权:有某种显示含义的数值
结点的带权路径长度:从树的根到该结点的路径长度(经过的边树)与该结点上权值的乘积。
树的带权路径长度:树中所有叶子结点的带权路径长度之和

5.3.2哈夫曼树的定义

在含有n个带权叶结点的二叉树中,其中带权路径长度最小的二叉树成为哈夫曼树,也称最优二叉树。

哈夫曼树的构造
在这里插入图片描述

5.3.3哈夫曼编码

字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值。

在这里插入图片描述

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值