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

数据结构——第五章 树

第五章 树

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编码

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

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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值