笔记:数据结构——第五章 树

(王道408考研数据结构)第五章树-第一节:树的定义、基本用语和常考性质 文章目录一:树基本概念(1)树的定义 一:树基本概念 (1)树的定义 树(Tree):这是一种非线性结构。是nnn(n≥0n\geq 0n≥0)个有限结点组成的一个具有层次关系的集合,与现实生活中的数十分相像,只不过它是倒挂的。nnn=0时称这样的树为空树 树结构特点:在任何一个非空树中 有且只有一个特定的称为根(Root)的结点 当nnn>1时,其余结点可以分为mmm(mmm>0)个互不相交的有限集合T1T_{1}T1​、T2T_{2}T2​…TmT_{m}Tm​,其中每一个集合本身又是一 阅读详情

数据结构——第五章 树

第五章 树

5.1 树的基本概念

5.1.1 树的定义和基本术语

  • 空树:结点数为0的树

  • 非空树:有且仅有一个根结点;

    没有后继的结点称为“叶子结点”或终端结点,有后继的结点称为“分支节点”或非终端结点

    除了根结点外,任何一个结点都有且仅有一个前驱,每个结点可以有0个或多个后继

  • 树是n个结点的有限集合;n=0时称为空树;

  • 任意一棵非空树中应满足:有且仅有一个特定的成为根的结点

    当n>1时,其余结点可分为m个互不相交的有限集合,其中每个集合本身又是一棵树,并且称为根结点的子树

  • 结点之间的关系描述

    孩子、双亲结点:结点的子树的根为该结点的孩子,该结点称为孩子的双亲

    兄弟结点:具有共同的双亲

    堂兄弟结点:双亲在同一层的结点

    祖先结点:从根到该节结点所经分支上的所有结点

    子孙结点:以某结点为根的子树中的任一结点

  • 两个结点之间的路径:只能从上往下;路径长度:经过几条边

  • 结点、树的属性描述

    结点的层次(深度)——从上往下数(默认从1开始)

    结点的高度——从下往上数;树的高度(深度)——总共多少层

    结点的度——有几个孩子(分支);树的度——各结点的度的最大值

  • 有序树VS无序树:逻辑上看,各子树是否有序,位置是否可互换

  • 树VS森林:由m(m>=0)个互不相交的树组成森林

5.1.2 树的性质

  1. 树中的结点数等于所有结点的度数之和加1

  2. 度为m的树、m叉树的区别

    树的度(度为m的数)——各结点的度的最大值

    m叉树(m叉树)——每个结点最多只能有m个孩子

  3. 度为m的树、m叉树第i层上至多有m^i-1个结点

  4. 高度为h的m叉树至多有(m^h-1)/(m-1)个结点(等比数列求和)

  5. 高度为h的m叉树至少有h个结点

    高度为h的、度为m的树至少有h+m-1个结点

  6. 具有n个结点的m叉树的最小高度为[log以m为底(n(m-1)+1)]

5.2 二叉树的概念

5.2.1 二叉树的定义和基本术语

  • 二叉树是n个结点的有限合集;或者为空二叉树m=0

    或者由一个根结点和两个互不相交的成为根的左子树和右子树组成,左子树和右子树有分别是一棵二叉树

  • 特点:每个结点至多只有两棵子树;左右子树不能颠倒(有序树)

  • 几个特殊的二叉树满二叉树

    1. 一棵深度为k且有2的k次方-1个结点的二叉树
    2. 完全二叉树:深度为k的具有n个结点的二叉树,当且仅当每一个编号都与深度k的满二叉树一一对应
    3. 二叉排序树:左子树上的结点的关键字均小于根结点的关键字,右子树大于
    4. 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1

5.2.2 二叉树的性质

  • 二叉树:n 0=n 2+1,度为0的结点比度为1的多一个结点

    第i层至多有2(i-1)个结点;高度为h的二叉树至多有2h-1个结点

  • 完全二叉树:具有n个结点的完全二叉树的高度h为[log 2 (n+1)]或[log 2 n]+1

    完全二叉树可以由的结点数n推出为0、1和2的结点个数为n 0、n 1和n 2(突破点:完全二叉树最多只会有一个度为1的结点)

5.2.3 二叉树的存储结构

*二叉树的顺序存储
#define MaxSize 100
struct TreeNode{			
   ElemType value;						 //结点中的数据元素
   bool isEmpty;  						 //结点是否为空
}

main(){
   TreeNode t[MaxSize];
   for (int i=0; i<MaxSize; i++){		//初始化时所有结点标记为空		
      t[i].isEmpty = true;
   }
}
  • t[0]:可以让第一个位置空缺,保证数组下标和结点编号一致

    t[1]:根结点位置

  • 几个重要常考的基本操作:i的左孩子——2 i;i的右孩子——2 i+1;i的父结点——[i/2];i所在的层次——[log 2 (n+1)]或[log 2 n]+1

  • 完全二叉树中共有n个结点,则:判断i是否有左孩子——2 i<=n;是否有右孩子——2 i+1<=n;是否是叶子/分支结点——i>[n/2]

  • 二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来

  • 非完全二叉树中共有n个结点,则:判断i是否有左孩子和右孩子——通过isEmpty判断

  • 结论:二叉树的顺序存储结构,只适合存取完全二叉树

*二叉树的链式存储
typedef struct BiTnode{
   ElemType data;          				//数据域
   struct BiTNode *lchild, *rchild; 	//左、右孩子指针
}BiTNode, *BiTree;

  • n个结点的二叉链表共有n+1个空链域
struct ElemType{
   int value;							//定义每一个ElemType里只有一个int型的变量
};

typedef struct BiTnode{
   ElemType data;         				//数据域
   struct BiTNode *lchild, *rchild; 	//左、右孩子指针
}BiTNode, *BiTree;

//定义一棵空树
BiTree root = NULL;

//插入根节点
root = (BiTree) malloc (sizeof(BiTNode));
root -> data = {1};
root -> lchild = NULL;
root -> rchild = NULL;

//插入新结点
BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
p -> data = {2};
p -> lchild = NULL;
p -> rchild = NULL;
root -> lchild = p; 					//作为根节点的左孩子
  • 找到指定结点p的左右孩子——运用左右孩子指针

  • 找到指定结点p的父结点——只能从根开始遍历寻找(复杂)或运用三叉链表

typedef struct BiTnode{
   ElemType data;        				//数据域
   struct BiTNode *lchild, *rchild; 	//左、右孩子指针
   struct BiTNode *parent;          	//父节点指针
}BiTNode, *BiTree;

5.3 二叉树的遍历

5.3.1二叉树的先中后序遍历

  • 遍历:按照某种次序把所有结点都访问一遍

    层次遍历:基于树的层次特性确定的次序规则

  • 先序遍历(DLR根左右)、中序遍历(LDR左根右)、后序遍历(LRD左右根)

  • 二叉树的遍历(手算):分支结点逐层展开法

  • 算数表达式的“分析树”:先序遍历——前缀表达式;中序遍历——中缀表达式(要加界限符);后序遍历——后缀表达式

*先序遍历
typedef struct BiTnode{
   ElemType data;          
   struct BiTNode *lchild, *rchild; 
}BiTNode, *BiTree;

void PreOrder(BiTree T){
   if(T!=NULL){
      visit(T);               			//访问根结点
      PreOrder(T->lchild);      		//递归遍历左子树
      PreOrder(T->rchild);      		//递归遍历右子树
   }
}
*中序遍历
typedef struct BiTnode{
   ElemType data;          
   struct BiTNode *lchild, *rchild; 
}BiTNode, *BiTree;

void InOrder(BiTree T){
   if(T!=NULL){
      InOrder(T->lchild);       		//递归遍历左子树
      visit(T);                 		//访问根结点
      InOrder(T->rchild);       		//递归遍历右子树
   }
}
*后序遍历
typedef struct BiTnode{
   ElemType data;          
   struct BiTNode *lchild, *rchild; 
}BiTNode, *BiTree;

void PostOrder(BiTree T){
   if(T!=NULL){
      PostOrder(T->lchild);       		//递归遍历左子树    
      PostOrder(T->rchild);       		//递归遍历右子树
      visit(T);                 		//访问根结点
   }
}
  • 遍历思想:想象补上空结点,从根结点出发,画一条路

    如果左边还没有走的路,优先往左走;走到路的尽头(空结点)就往回走

    如果左边没路了,就往右边走;如果左、右边都没路了,则往上走

  • 每一个结点都会被路过三次:先序遍历——第一次路过时访问结点;中序遍历——第二次路过时访问结点;后序遍历——第三次路过时访问结点

*求树的深度(应用)
int treeDept(BitTree T){
    if(T==NULL)
        return 0;
    else{
        int l=treeDept(T->lchild);
        int r=treeDept(T->rchild);
        return l>r ? l+1:r+1			//树的深度=Max(左子树深度,右子树深度)+
    }
}
  • 树的深度:左子树和右子树中高的加根结点的高度

5.3.2 二叉树的层序遍历

  • 算法思想:
    1. 初始化一个辅助队列
    2. 根结点入队
    3. 若队列非空,则队头结点出队,并将其左右孩子插入队尾(若有)
    4. 重复步骤3直至队列为空
*层序遍历
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);
    }
}

//二叉树的结点(链式存储)
typedef struct BiTNode{
    char data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

//链式队列结点
typedef sruct LinkNode{
    BiTNode *data;
    struct LinkNode *next;
}LinkNode;

type struct{
    LinkNode *front,*rear;				//队头、队尾指针
}

5.3.3 由遍历序列构造二叉树

  • 一个前序、中序和后序遍历序列可能对应多种二叉树形态
  • 前序、后序、层序+中序遍历序列可以构造出二叉树
*前序+中序遍历序列
  • 前序遍历:(根结点)(左子树的前序遍历序列)(右子树的前序遍历序列)
  • 中序遍历:(左子树的中序遍历)(根结点)(右子树的中序遍历)
*后序+中序遍历序列
  • 后序遍历:(左子树的前序遍历序列)(右子树的前序遍历序列)(根结点)
  • 中序遍历:(左子树的中序遍历)(根结点)(右子树的中序遍历)
*层序+中序遍历序列
  • 层序遍历:(根结点)(左子树的根)(右子树的根)…
  • 中序遍历:(左子树的中序遍历)(根结点)(右子树的中序遍历)

5.3.4 线索二叉树的概念

  • 二叉树的中序遍历都必须从根结点出发,若从一指定结点开始无法进行中序遍历

  • 找到指定结点p在中序遍历中的前驱,思路:

    从根结点出发,重新进行中序遍历,指针q记录当前访问的结点,指针pre记录上一个结点

    当qp时,pre为前驱;当prep时,q为后继

  • 缺点:不方便,遍历操作必须从根开始

*中序线索二叉树
  • n个结点的二叉树,有n+1个空链域(在没有孩子的结点上)!可用来记录前驱、后继的信息
  • 指向前驱、后继的指针称为“线索”,前驱线索由左孩子充当,后继线索由右孩子充当
  • 线索二叉树的存储结构
//二叉树的结点(对比)
typedef struct BiTNode{
    ElemType data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

//线索二叉树的结点
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag   					//左右线索标志
}ThreadNode,*ThreadTree;
  • tag0,表示指针指向孩子;tag1,表示指针是“线索”

  • 中序线索二叉树——线索指向中序前驱、中序后继(以中序遍历方法其后继结点)

  • 先序、后序线索二叉树与中序类似

5.3.5 二叉树的线索化

*一般方法找到中序前驱
//中序遍历
void InOrder(BiTree T){				//最好改一个函数名findPre
    if(T!=NULL){
        InOrder(T->lchild);
        visit(T);
        InOrder(T->rchild);
    }
}

//访问结点q
void visit(BiTNode *q){
    if(p==q)
        final=pre;
    else
        pre=q;
}

//辅助全局变量,用于查找结点p的前驱
BiTNode *p;							//p指向目标结点
BiTNode *pre=NULL;					//指向当前访问结点的前驱
BiTNode *final=NULL;				//全局变量;用于记录最终结果
*中序线索化
  • 初步建成的树ltag、rtag=0
  • 左孩子和右孩子为空时,建立前驱后继指针,并令tag=1
  • 最后检查pre的rchild是否为NULL,如果是,则令rtag=1
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;

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

//线索二叉树结点
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag;							//左、右线索标志
}ThreadNode,*ThreadTree;

//中序遍历二叉树,一边遍历一遍线索化
void InThread(ThreadTree T){
    if(T!=NULL){
        InTread(T->lchild);
        visit(T);
        InTread(T->rchild);
    }
}

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 p,ThreadTree &pre){
    if(p!=NULL){
        InThread(p->lchild,pre);			//递归,线索化左子树
        if(p->lchild==NULL){
            p->lchild=pre;
            p=>ltag=1;
        }
        if(pre!=NULL&&pre->rchild==NULL){
            pre->rchild=p;
            pre->rtag=1;
        }
        pre=p;
        InThread(p->rchild,pre);			//递归,线索化右子树
    }
}

//中序线索化二叉树T
void CreatInThread(ThreadTree T){
    ThreadTree pre=NULL;					//区别:定义为CreatInThread的局部变量;InThread加引用,才能让其局部变量起到全局变量的作用
    if(T!=NULL){
        InThread(T,pre);						//线索化二叉树
        pre->rchild=NULL;					//处理遍历的最后一个结点;线索化遍历完一定是最后一个右孩子,可以不用判断
        pre->rtag=1;
    }
}
*先序线索化
//全局变量pre,指向当前访问结点的前驱
TheardNode *pre=NULL;

//先序线索化二叉树T(同中序线索化)
void CreatPreThread(ThreadTree T){
    pre=NULL;
    if(T!=NULL){
        InThread(T);
        if(pre->rchild==NULL)
            pre->rtag=1;
    }
}

//先序遍历二叉树,一边遍历一遍线索化
void PreThread(ThreadTree T){
    if(T!=NULL){
        visit(T);
        if(T->ltag==0)						//不是前驱节点时,才遍历左子树;否则会一直转圈循环
            InTread(T->lchild);
        InTread(T->rchild);
    }
}

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 PreThread(ThreadTree p,ThreadTree &pre){
    if(p!=NULL){
        if(p->lchild==NULL){				//visit函数
            p->lchild=pre;
            p=>ltag=1;
        }
        if(pre!=NULL&&pre->rchild==NULL){
            pre->rchild=p;
            pre->rtag=1;
        }
        pre=p;
        if(T->ltag==0)						//左子树访问
            PreThread(p->lchild,pre);	
        PreThread(p->rchild,pre);			//右子树访问		
    }
}

//先序线索化二叉树T
void CreatPreThread(ThreadTree T){
    ThreadTree pre=NULL;			
    if(T!=NULL){
        PreThread(T,pre);					
        if(pre->rchild==NULL)			
        	pre->rtag=1;
    }
}
*后序线索化
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;

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

//后序遍历二叉树,一边遍历一遍线索化
void PostThread(ThreadTree T){
    if(T!=NULL){
        InTread(T->lchild);
        InTread(T->rchild);
        visit(T);							//访问结点q时,左子树眼镜处理完,不会出现转圈问题
    }
}

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;
}
  • 另一种写法相同

5.3.6 线索二叉树——找前驱/后继

*中序线索二叉树——找后继
  • 若``p->rtag==1,说明其没有右孩子,线索化后next=p->rchild`

  • 若p->rtag==0,说明有右孩子,求其中序遍历的第一个访问结点,即next=p的右子树最左下结点

//找到以p为根的子树中,第一个被遍历的结点
TheardNode *Firstnode(TheardNode *p){
    while(p->ltag==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);
}
  • 不需要递归调用函数,时间复杂度为O(1)
*中序线索二叉树——找前驱
  • 若p->ltag==1,说明其没有左孩子,线索化后next=p->lchild

  • 若p->ltag==0,说明有左孩子,即next=p的左子树最右下结点

//找到以p为根的子树中,最后一个被遍历的结点
TheardNode *Lastnode(TheardNode *p){
    while(p->rtag==0)						//循环找到最右下的结点(不一定是叶结点)
        p=p->rchild;
    return p;
}

//在中序线索二叉树中找到结点p的前驱节点
ThreadNode *Prenode(ThreadNode *p){
    if(p->ltag==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->rtag==1,说明其没有右孩子,线索化后next=p->rchild

  • 若p->rtag==0,说明有右孩子,(根左右)但不清楚左孩子

    若p有左孩子,则先序后继为左孩子;若p没有左孩子,则先序后继为右孩子

*先序线索二叉树——找前驱
  • 若p->ltag==1,说明其没有左孩子,线索化后next=p->lchild

  • 若p->ltag==0,说明有左孩子,(根左右)左右子树都是其后继,只能从头开始先序遍历

  • 改为三叉链表,增加指向其父结点的指针:

    1. p为其父节点的左孩子,前驱是p的父节点
    2. p为其父节点的右孩子,且p的父节点没有左孩子,前驱是p的父节点
    3. p为其父节点的右孩子,且p的父节点有左孩子,前驱是左兄弟子树的最后一个先序遍历结点(根节点出发,先往右,右没有往左,找到最后一层的结点)
    4. p没有父节点,为根结点,则没有先序前驱
*后序线索二叉树——找前驱
  • 若p->ltag==1,说明其没有左孩子,线索化后next=p->lchild

  • 若p->ltag==0,说明有左孩子,(左右根)但不清楚右孩子

    若p有右孩子,则后序前驱为右孩子;若p没有右孩子,则后序前驱为左孩子

*后序线索二叉树——找后继
  • 若p->ltag==1,说明其没有左孩子,线索化后next=p->lchild

  • 若p->ltag==0,说明有左孩子,(左右根)左右子树都是其前驱,只能从头开始先序遍历

  • 改为三叉链表,增加指向其父结点的指针:

    1. p为其父节点的右孩子,后继是p的父节点
    2. p为其父节点的左孩子,且p的父节点没有右孩子,后继是p的父节点
    3. p为其父节点的左孩子,且p的父节点有右孩子,后继是右兄弟子树的第一个后序遍历结点(根节点出发,先往左,左没有往右,找到最后一层的结点)
    4. p没有父节点,为根结点,则没有后序后继
  • 总结:先序线索二叉树找前驱、后序线索二叉树找后继只能用三叉链表或再根结点遍历寻找

5.4 树、森林

5.4.1 树的存储结构

  • 树是一种递归定义的数据结构
*双亲表示法(顺序存储)
  • 每个结点中保存指向双亲的“指针”
  • 根结点固定存储在0;指针-1表示没有双亲
#define MAX_TREE_SIZE 100  			//树中最多结点数
typedef struct{      				//树的结点定义
   ElemType data; 
   int parent;      				//双亲位置域
}PTNode;

typedef struct{                   	//树的类型定义
   PTNode nodes[MAX_TREE_SIZE];   	//双亲表示
   int n;                         	//结点数
}PTree;

  • 增:新增数据元素,无需按逻辑上的次序存储

  • 删(叶子结点): 将伪指针域设置为-1;用尾部的数据填补,需要更改结点数n

  • 删(子树):同时删除它的子孙结点,需用到查找

  • 查询:查指定结点的双亲用指针;查指定结点的孩子只能从头遍历,指针-1法产生的空数据会导致遍历更慢

  • 二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来

    i的左孩子——2i;i的右孩子——2i+1;i的父节点——[i/2]

*孩子表示法(顺序+链式存储)
  • 顺序存储各个节点,每个结点中保存孩子链表头指针
struct CTNode{
   int child;    				//孩子结点在数组中的位置
   struct CTNode *next;    		//下一个孩子
};

typedef struct{
   ElemType data;
   struct CTNode *firstChild;   //第一个孩子
}CTBox;

typedef struct{
   CTBox nodes[MAX_TREE_SIZE];
   int n, r;   					//结点数和根的位置
}CTree;
*孩子兄弟表示法(链式存储)
typedef struct CSNode{
   ElemType data;                               //数据域
   struct CSNode *firstchild, *nextsibling;     //第一个孩子和右兄弟指针, *firstchild 看作左指针,*nextsibling看作右指针
}CSNode. *CSTree;
  • 用孩子兄弟表示法存储的树在物理上呈现出“二叉树”的样子
*森林和二叉树的转换
  • 类似孩子兄弟表示法——左孩子右兄弟
  • 森林转换为二叉树:各个树的根结点视为兄弟关系
  • 二叉树转换为森林:本质是用二叉链表存储森林

5.4.2 树和森林的遍历

*树的先根遍历
  • 先根遍历:若树非空,先访问根节点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
void PreOrder(TreeNode *R){
   if(R!=NULL){
      visit(R);    				//访问根节点
      while(R还有下一个子树T)
         PreOrder(T);      	//先跟遍历下一个子树
   }
}
*树的后根遍历
  • 后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再返回根节点(与对应二叉树的中序遍历序列相同)
void PostOrder(TreeNode *R){
   if(R!=NULL){
      while(R还有下一个子树T)
         PostOrder(T);      	//后跟遍历下一个子树
      visit(R);    				//访问根节点
   }
}
*树的层序遍历
  • 层序遍历(队列实现)

    1. 若树非空,则根结点入队
    2. 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
    3. 重复以上操作直至队尾为空
  • 树的先序遍历和后序遍历——深度优先遍历;树的层序遍历——广度优先遍历

  • 树的后根遍历序列与这棵树相应二叉树的中序序列相同

*森林的先序遍历
  • 依次对各个树进行先根遍历

  • 也可以先转换成与之对应的二叉树,对二叉树进行先序遍历

*森林的中序遍历
  • 依次对各个树进行后根遍历

  • 也可以先转换成与之对应的二叉树,对二叉树进行中序遍历

  • 树——森林——二叉树

    1. 先根遍历——先序遍历——先序遍历
    2. 后根遍历——中序遍历——中序遍历

5.5 二叉树的应用

5.5.1 二叉排序树

*二叉排序树的定义
  • 二叉排序树,又称二叉查找树

    左子树所有结点关键字均小于根结点的关键字;右子树所有结点关键字均大于根结点的关键字

    左子树和右子树又各是一棵二叉排序树

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

  • 进行中序遍历,可以得到一个递增的有序序列

*二叉排序树的查找
  • 若树非空,目标值与根结点的值比较:

    若相等,则查找成功

    若小于根结点,则在左子树上查找,否则在右子树上查找

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

//在二叉排序树中查找值为key的结点(非递归)
//最坏空间复杂度:O(1)
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;
}

//在二叉排序树中查找值为key的结点(递归)
//最坏空间复杂度:O(h)
BSTNode *BSTSearch(BSTree T, int key){
   if(T == NULL)
      return NULL;		//查找失败
   if(key == T->key)
      return T;			//查找成功
   else if(key < T->key)
      return BSTSearch(T->lchild, key);		//在左子树中查找
   else 
      return BSTSearch(T->rchild, key);		//在右子树上查找
}
*二叉排序树的插入
  • 若原二叉树为空,则直接插入结点;否则:

    若关键字小于根结点值,则插入到左子树

    若关键字大于根节点值,则插入到右子树

  • 若存在相同关键字的结点,插入失败

  • 新插入的结点一定是叶子结

//在二叉排序树中插入关键字为k的新结点(递归)
//最坏空间复杂度:O(h)
int BST_Insert(BSTree &T, int k){		//此处参数为引用类型
   if(T==NULL){           //原树为空,新插入的结点为根结点
      T = (BSTree)malloc(sizeof(BSTNode));
      T->key = k;
      T->lchild = T->rchild = NULL;
      return 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);
}
*二叉排序树的构造
//按照str[]中的关键字序列建立二叉排序树
void Crear_BST(BSTree &T, int str[], int n){
   T = NULL;                     //初始时T为空树
   int i=0;
   while(i<n){
      BST_Insert(T,str[i]);     //依次将每个关键字插入到二叉排序树中
      i++;
   }
}
  • 不同关键字序列可能得相同的二叉排序树
*二叉排序树的删除
  • 先搜索找到目标结点

    1. 若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质

    2. 若结点z只有一棵左子树或右子树,则让z的子树称为z父节点的子树,替代z的位置

    3. 若结点z有左右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况

      (z的后继:z的右子树中最左下的结点;该节点一定没有左子树)

      (z的前驱:z的左子树中最右下的结点;该节点一定没有右子树)

*查找效率分析
  • 查找长度——在查找运算中,需要对比关键字的次数;反映了查找操作时间复杂度

  • 查找成功的平均查找长度ASL:(总对比次数)/(总结点数)

  • 若树高为h,则找到最下层的一个结点需要对比h次,时间复杂度为0(h)

    最好情况:n个结点的二叉树最小高度为[log2n]+1;平均查找长度O(log2n)

    最坏情况:每个结点只有一个分支,树高h等于结点数n;平均查找长度O(n)

  • 平衡二叉树:树上的任一结点的左子树和右子树的深度之差不超过1(最好情况)

  • 查找失败的平均查找长度ASL:补充失败结点后(总对比次数)/(总空结点数)

5.5.2 平衡二叉树

*平衡二叉树的定义
  • 平衡二叉树,简称平衡树(AVL树)

    树上的任一结点的左子树和右子树的高度之差不能超过1

  • 结点的平衡因子=左子树高-右子树高

    平衡二叉树结点的平衡因子的值只可能是-1,0或1

//平衡二叉树结点
typedef struct AVLNode{
   int key;         //数据域
   int balance;     //平衡因子
   struct AVLNode *lchild; *rchild; 
}AVLNode, *AVLTree;
*平衡二叉树的插入
  • 调整插入后的“不平衡”问题:

    从插入点往回找到第一个不平衡结点,调整以该节点为根的子树

    每次调整的对象都是“最小不平衡子树”

  • 调整最小不平衡子树

    目标:恢复平衡;保持二叉排序树

    1. LL: 在A结点的左孩子的左子树中插入导致不平衡

      调整: A的左孩子B结点右上旋,A结点右下旋称为B的右子树的根结点,而B的右子树作为A的左子树

      代码思路:指针f指向A结点,指针p指向B结点(A的左孩子),指针gf指向A结点的父节点
          1. f->lchild=p->rchild;			//A的左子树变成(指向)B的右孩子
      	2. p->rchild=f;					//B的右孩子指向A
          3. gf->lchild/rchild=p;			//A的父节点变成B的父节点
      
    2. RR: 在A结点的右孩子的右子树中插入导致不平衡

      调整: A的右孩子B结点左上旋,A结点左下旋称为B的左子树的根结点,而B的左子树作为A的右子树

      代码思路:指针f指向A结点,指针p指向B结点(A的右孩子),指针gf指向A结点的父节点
          1. f->rchild=p->lchild;			
      	2. p->lchild=f;					
          3. gf->lchild/rchild=p;			
      
    3. LR: 在A结点的左孩子的右子树中插入导致不平衡

      调整: A的左孩子B的右孩子C的根结点,先左上旋提升到B,再右上旋提升到A

    4. RL: 在A结点的右孩子的左子树中插入导致不平衡

      调整: A的右孩子B的左孩子C的根结点,先右上旋提升到B,再左上旋提升到A

  • 只有左孩子才能右上旋,只有右孩子才能左上旋

*查找效率分析
  • 高为h的平衡二叉树最少有几个结点——递推求解
  • 平衡二叉树最大深度为0(logn),平均查找长度/查找的时间复杂度为O(logn)

5.5.3 哈弗曼树

*带权路径长度
  • 结点的权:有某种实现含义的数值(如:表示结点的重要性)
  • 结点的带权路径长度:从根结点到该结点的路径长度(经过的边数)与该结点的权值的乘积
  • 树的带权路径长度:树中所有叶结点的带权路径长度之和,记为WPL = ∑ wi li (n;i=1)
    式中, wi是第 i个叶子结点的权值,li是根结点到第 i个叶子结点的路径长度
*哈弗曼树的定义
  • 在含有n个带权叶子结点的二叉树中,带权路径长度 (WPL) 最小的二叉树称为哈夫曼树,也称为最优二叉树
*哈弗曼树的构造
  • 给定n个权值分别为 w 1 , w 2 , . . . , w n的结点,构造哈弗曼树的算法描述如下:
    1. 将这n结点分别作为n棵仅含有一个结点的二叉树,构成森林F
    2. 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和
    3. 从 F中删除选出的两棵树,同时将新得到的树加入森林 F中
    4. 重复第 2、3 步骤,直到森林 F中只剩下一棵树为止
  • 哈弗曼树的性质:
    1. 每个初始结点最终都成为叶子结点,且权值越小的结点到根结点的路径长度越大
    2. 构造过程共新建结点n-1个,因此哈夫曼树的结点总数为2n-1个
    3. 哈夫曼树中不存在度为 1 的结点;每次构造新树都选择两棵树作为左右子树
    4. 哈弗曼树并不唯一,但WPL必然相同且为最优
*哈弗曼编码
  • 由哈弗曼树得到哈弗曼编码

    字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,再构造哈弗曼树

  • 固定长度编码——每个字符用相同长度的二进制为表示——ASCII编码

  • 可变长度编码——允许对不同字符用不等长的二进制位表示——哈弗曼编码

  • 前缀编码——没有一个编码是另一个编码的前缀

游戏开发实战:用UE4 Marschner模型打造次世代头发渲染(附Shader代码解析) 本文详细解析了如何在UE4中使用Marschner模型实现次世代头发渲染,包括Shader代码逐行解读与参数调优。通过对比传统Kajiya-Kay模型,展示了Marschner模型在物理精确性和视觉效果上的优势,并提供了性能优化与实战技巧,帮助技术美术师提升游戏角色渲染品质。 阅读详情

相关推荐

Python爬虫实战:逆向解析央视频加密参数(附完整代码)

本文详细解析了央视频加密体系的技术实现,通过Python逆向工程实战演示如何解密动态密钥、AES加密和自定义哈希算法等核心组件。文章提供完整代码示例,涵盖GUID生成、参数构造和异步请求处理,帮助开发者掌握现代视频平台的反爬机制破解方法。

weixin_42532473的博客 1133

线索二叉树

(1)若X是后序线索二叉树中的叶结点,且X存在左兄弟结点Y,则X的右线索指向的是( )。 根据题意,x存在左兄弟结点y,说明x是右边的那个结点 由于是rg

cuisl37186486的博客 1607

Simulink单相桥式有源逆变电路仿真:从器件选型到波形分析(MATLAB 2018a实战)

本文基于MATLAB 2018a的Simulink平台,详细演示了单相桥式有源逆变电路的完整仿真流程。从Simscape/Power Systems库的器件选型、参数配置,到触发脉冲设置与电路连接,逐步指导搭建仿真模型。通过调整触发角α,直观展示了电路从整流到有源逆变的工作状态切换,并深入分析了负载电压、电流波形,以及逆变颠覆现象的产生条件与特征。

cake8的博客 1162

寻找左兄弟节点

寻找左兄弟节点,与寻找父节点一样,在判断右兄弟节点是否等于某个值,如果等于,直接返回左兄弟节点,实现过程如下所示: package cn.edu.nwu.structs.tree; /** * @author jcm * 找父节点 * 时间 2016年9月1日 */ public class GetLeftBrotherTreeNode { public static int left

猛龙过蒋的专栏 1156

如何设计一个处理物联网设备数据流的后端系统。

设计一个处理物联网设备数据流的后端系统,需要综合考虑设备数据采集、传输、处理、存储等多个方面,保证系统能够处理海量数据,具备高可用性、扩展性和安全性。合理选择适当的技术架构和工具(如MQTT、Kafka、InfluxDB等),并确保系统能够灵活应对不断变化的业务需求和设备接入。

weixin_53227829的博客 841

AD56XXR系列DAC驱动设计:SPI接口、寄存器配置与嵌入式工程实践

数模转换器(DAC)是嵌入式系统实现高精度模拟控制的核心器件,其性能取决于通信协议可靠性、寄存器配置准确性及硬件协同设计能力。基于SPI总线的电压输出型DAC广泛应用于工业控制、光模块偏置、传感器校准等场景,需兼顾静态精度、动态响应与低功耗特性。AD56XXR系列作为典型集成内部基准与多通道同步更新能力的12/14/16位DAC,支持轨到轨输出与灵活掉电模式,为资源受限MCU提供可移植固件范式。本文围绕SPI时序约束、16位指令字解析、LDAC同步机制及精度补偿策略展开,覆盖从硬件连接、初始化流程到量产级可

weixin_33481022的博客 612

java heap 实现_斐波那契堆(Fibonacci heap)原理详解(附java代码实现)

前言斐波那契堆(Fibonacci heap)是计算机科学中最小堆有序树的集合。它和二项式堆有类似的性质,但比二项式堆有更好的均摊时间。堆的名字来源于斐波那契数,它常用于分析运行时间。堆结构介绍基本术语介绍:关键字:堆节点储存的用于比较的信息度数:堆节点拥有的孩子数(注意,不包括孩子的孩子)左兄弟:节点左边的兄弟节点右兄弟:节点右边的兄弟节点mark:是否有孩子节点被删除斐波那契堆是一系列无序树的...

weixin_42120275的博客 358

28.Anaconda安装jupyter notebook、jupyterlab及体验

手贱,不小心将Anaconda自带得jupyter给卸载了,没法子了查了好多资料都比较麻烦,所以自己摸索着重新安装,记录一下心得。说明:Anaconda没有加入环境变量,Anaconda官方建议不要将Anaconda加入环境变量,这就意味着他和python是可以共存的。

迷逝 5031

数据结构——树

树是`n(n≥0)`个结点的有限集合,一种递归的数据结构,同时也是一种分层结构。

知归 1036

YOLOv10改进策略【模型轻量化】| MoblieNetV3:基于搜索技术和新颖架构设计的轻量型网络模型

本文记录的是基于MobileNet V3的YOLOv10目标检测轻量化改进方法研究。的模型结构是通过得来的,其中的基础模块结合了的深度可分离卷积、的线性瓶颈和倒置残差结构以及MnasNet中基于挤压和激励的轻量级注意力模块,使模型在性能、效率和灵活性方面都具有显著的优势。本文在替换骨干网络中配置了原论文中的small和large2种模型,以满足不同的需求。

Limiiiing的博客 515

2023,程序员的出路在哪里?

可是似乎从30岁开始这工资就没涨过,前面跳槽几次涨的薪水在30岁以后跳槽已经不管用了、定格了,这几年由于市场环境不景气,程序员竞争越来越激烈,36岁跟30拿的工资一模一样,不仅工资拿的一样,反而事情还越来越多了,这些都意味着什么?处理故障需要的通常不仅仅是写代码的能力,还需要对一个系统的全貌要有一定的掌握。虽然目前不少大型互联网企业都在进行结构性调整,但是从互联网行业发展的基本面来看,未来在产业互联网发展的过程中,IT行业和传统行业将会释放出大量的就业岗位,所以未来程序员的发展空间还是非常值得期待的。

AI天才研究院 20万+

【数据结构】二叉树

树的概念及结构左孩子右兄弟表示法双亲表示法二叉树的结构及概念满二叉树和完全二叉树二叉树的顺序结构及实现堆的概念及实现构造堆结构插入数据到堆堆的向下调整算法向下调整算法如何创建一个堆堆的向上调整算法向上调整算法创建一个堆删除堆顶元素访问堆顶元素向上调整建堆和向下调整建堆的时间复杂度4.二叉树的链式结构及实现前序遍历中序遍历后序遍历层序遍历已知前序序列和中序序列重建二叉树问题

m0_64538862的博客 2642

GEE:基于MODIS数据计算NDVI时间序列,并使用Savatsky-Golay(SG)滤波器对其进行平滑处理

优快云@_养乐多_本文将介绍如何在 Google Earth Engine (GEE) 平台中,使用 MODIS 数据计算 NDVI 时间序列,并对该数据集应用 Savitzky-Golay 滤波器,得到了一个新的平滑后的数据集。Savitzky-Golay 滤波器可以有效地去除信号中的噪声,并平滑信号的变化。该方法可以扩展到基于 MODIS 数据计算的任何指数或者波段。

养乐多的博客 1426

数据结构笔记:第五章 树和二叉树

树定义: 在树中将数据元素称为节点。 树:n(n≥0)个结点的有限集合。 树的基本术语: 结点的度:结点所拥有的子树的个数。 树的度:树中各结点度的最大值。 叶子结点:度为0的结点,也称为终端结点。 分支结点:度不为0的结点,也称为非终端结点。 孩子结点:树中某结点子树的根结点称为这个...

qq_43632319的博客 425

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

408数据结构第五章 树与二叉树

本章内容较多,讲解了树、森林、并查集等多种数据结构及其操作原理与代码实现

m0_74983085的博客 2473

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

866数据结构笔记 - 第五章 树和二叉树

866数据结构、湖南大学考研、树和二叉树

qq_45331246的博客 3998

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

王道数据结构课代表 - 考研数据结构 第五章 树和二叉树 究极精华总结笔记

数据结构与算法 笔记导航🚥🚥🚥 🥬 第一章 绪论 🥕 第二章 线性表 🥪 第三章 栈和队列 🍊 第四章 串-KMP(看毛片算法) 🍒 第五章 树和二叉树 🍀 第六章 图 🍚 第七章 查找(B树、散列表) 🧄 第八章 排序 (内部排序:八大排序动图演示与实现 + 外部排序) 🍔 数据结构与算法 复试精简笔记 🎨 408 全套初复试笔记汇总 传送门 🏃‍🏃‍🏃‍ 如果本篇文章对大家起到帮助的话,跪求各位帅哥美女们,求赞👍 、求收藏、求关注!

clzZ2的博客 7569

王道数据结构-考研笔记

本数据结构笔记配合王道视频食用,效果更佳。若有条件,可彩印成册,涂写勾画,记出自己的笔记,内容若有错误,请见谅。本数据结构笔记内容绝大部分来自王道数据结构视频,部分来自《大话数据结构》,希望能够帮助到在考研路上的诸位。一路生花吧!

weixin_51297735的博客 6984

数据结构笔记(王道考研) 第五章:树和二叉树

数据结构笔记(王道考研) 第五章:树和二叉树

zimuzi2019的博客 3974

王道数据结构课代表 - 考研数据结构 第七章 查找(B树、散列表) 究极精华总结笔记(C版本)

数据结构与算法 笔记导航🚥🚥🚥 🥬 第一章 绪论 🥕 第二章 线性表 🥪 第三章 栈和队列 🍊 第四章 串-KMP(看毛片算法) 🍒 第五章 树和二叉树 🍀 第六章 图 🍚 第七章 查找(B树、散列表) 🧄 第八章 排序 (内部排序:八大排序动图演示与实现 + 外部排序) 🍔 数据结构与算法 复试精简笔记 🎨 408 全套初复试笔记汇总 传送门 🏃‍🏃‍🏃‍ 如果本篇文章对大家起到帮助的话,跪求各位帅哥美女们,求赞👍 、求收藏、求关注!

clzZ2的博客 8958

【数据结构】第五章:树与二叉树

【数据结构】第五章:树与二叉树 一、树的定义 1. 基本概念 2. 基本术语 3. 常见性质 二、二叉树的定义 1. 基本概念 2. 特殊二叉树 3. 常见性质 三、二叉树的存储结构 1. 顺序存储 2. 链式存储 四、二叉树的遍历 1. 先序遍历 2. 中序遍历 3. 后序遍历 4. 层序遍历 5. 由遍历序列构造二叉树 五、线索二叉树 1. 基本概念 2. 三种线索二叉树 3. 二叉树的线索化 4. 中序线索二叉树找前驱 / 后继 5. 先序线索二叉树找前驱 / 后继 6. 后序线索二叉树找前驱 / 后

iFulling的博客 2024

数据结构 第五章树和二叉树 思维导图

数据结构 第五章树和二叉树 思维导图

melody9494的专栏 1242

《王道》数据结构笔记整理2022

数据结构第一章绪论1.1数据结构的基本概念1.2数据结构的三要素1.3算法的基本概念1.4算法的时间复杂度1.5算法的空间复杂度 第一章绪论 1.1数据结构的基本概念 1.数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被程序识别和处理的符号的集合。 2.数据元素:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它由学号、姓名、性别等数据项组成。 3.数据对象:数

池鱼 27万+

408数据结构:树与二叉树选择题做题笔记

树与二叉树选择题做题笔记

m0_74983085的博客 1047

2023王道考研数据结构笔记第五章——树

二叉树是 n(n≥0)个结点的有限集合:①或者为空二叉树,即 n = 0。②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成,左子树和右子树又分别是一棵二叉树。二叉树的特点:①每个结点至多只有两棵子树。②左右子树不能颠倒(二叉树是有序树——注意区别:度为2的有序树二叉树的五种状态:①空二叉树②只有左子树③只有右子树④只有根节点⑤左右子树都有结点的权:有某种现实含义的数值(如:表示结点的重要性等)结点的带权路径长度。

xiamuandsansan的博客 911

数据结构(第五章 树与二叉树,学习笔记)

1.1 树的基本概念 1.1.1树的基本概念:   树:从树根生长,逐级分支,是一种递归定义的数据结构。   树是n(n≥0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树种应满足:     1.当且仅有一个特定的称为根的结点。     2.当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1T_1T1​,T2T_2T2​,…,TmT_mTm​,其中每个集合本身又是一棵树,并且称为根节点的子树。 1.1.2非空树的特性:   ①有且仅有一个根节点。   ②没有

weixin_42645653的博客 1563
上一篇: 笔记:数据结构——第四章 串
下一篇: 笔记:数据结构——第六章 图
ITS_Oaij
博客等级 码龄6年 1857粉丝 51原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值