数据结构-树(二)

178 阅读15分钟

5.3. 二叉树的遍历和线索二叉树

5.3.1_1 二叉树的先中后序遍历
  • 遍历:按照某种次序把所有结点都访问一遍

  • 二叉树的递归特性:

    ①要么是个空二叉树

    ②要么就是由“根节点+左子树+右子树”组成的二叉树

先序遍历:根左右(NLR)

中序遍历:左根右(LNR)

后序遍历:左右根(LRN)

  • 先序遍历(PreOrder)的操作过程如下:
  1. 若二叉树为空,则什么也不做;

  2. 若二叉树非空:

    ①访问根结点;

    ②先序遍历左子树;

    ③先序遍历右子树。

  • 代码实现如下:
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);      //递归遍历右子树
    }
}
  • 中序遍历(InOrder)的操作过程如下:
  1. 若二叉树为空,则什么也不做;

  2. 若二叉树非空:

①中序遍历左子树;

②访问根结点;

③中序遍历右子树;

  • 代码实现如下:
typedef struct BiTNode{  
    ElemType data;  
    struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
 
// 中序遍历
void PreOrder(BiTree T){  
    if(T!=NULL){     
        PreOrder(T->lchild);      //递归遍历左子树 
        visit(T);                 //访问根结点        
        PreOrder(T->rchild);      //递归遍历右子树
    }
}
  • 后序遍历(InOrder)的操作过程如下:
  1. 若二叉树为空,则什么也不做;

  2. 若二叉树非空: ①后序遍历左子树;

②后序遍历右子树;

③访问根结点。

  • 代码实现如下:
typedef struct BiTNode{  
    ElemType data;  
    struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
 
// 后序遍历
void PreOrder(BiTree T){  
    if(T!=NULL){     
        PreOrder(T->lchild);      //递归遍历左子树 
        PreOrder(T->rchild);      //递归遍历右子树
        visit(T);                 //访问根结点        
    }
}
  • 应用:求树的深度
int treeDepth(BiTree T){  
    if(T == NULL){  
        return 0;  
    }else{      
        int l = treeDepth(T->lchild); 
        int r = treeDepth(T->rchild); 
        //树的深度=Max(左子树深度,右子树深度)+1
        return l>r ? l+1 r+1;  
    }
}

5.3.1_2 二叉树的层次遍历
  • 算法思想:

①初始化一个辅助队列

②根结点入队

③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)

④重复③直至队列为空

  • 代码实现:
// 二叉树结点(链式存储)
typedef struct BiTNode{  
    char data;   
    struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
 
// 链式队列结点
typedef struct LinkNode{ 
    BiTNode *data;             //存结点的指针而不是结点本身
    struct LinkNode *next;
}LinkNode;
 
typedef struct{ 
    LinkNode *front, *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);     //右孩子入队
    }
}

5.3.1_3 由遍历序列构造二叉树
  • 若只给出一棵二叉树的 前/中/后/层 序遍历序列中的一种,不能唯一确定一棵二叉树 一种遍历序列可能对应多种形态

前序 + 中序遍历序列

由前序遍历可推出根节点在中序遍历中的位置,从而确定左右结点,再依此类推

后序 + 中序遍历序列

由后序遍历可推出根结点在中序遍历中的位置,从而确定左右结点在中序遍历的位置

层序 + 中序遍历序列

由层序遍历可推出根结点,再根据根节点进一步确定左右的根的位置

5.3.2_1 线索二叉树的概念
  • 普通二叉树进行遍历时,找前驱、后继很不方便,且每次都要从根结点出发,无法从一个指定的结点开始遍历。

  • n 个结点的二叉树,有 n+1 个空链域,可用来记录前驱、后继的信息。 指向前驱、后继的指针被称为“线索”,形成的二叉树就称为线索二叉树。

  • 线索二叉树的存储

  • 线索二叉树的结点在原本二叉树的基础上,新增了左右线索标志 tag。

  • tag == 0 时,表示指针指向孩子;tag == 1 时,表示指针是“线索”。

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

5.3.2_2 二叉树的线索化
  • 中序线索化
//线索二叉树结点
typedef struct ThreadNode{
    ElemType data;  
    struct ThreadNode *lchild, *rchild;   
    int ltag, rtag;            //左、右线索标志
}ThreadNode, *ThreadTree;
 
// 中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T) {
	if (T != NULL) {
		InThread(p->lchild);    //中序遍历左子树
		visit(T);               //访问根结点
		InThread(p->rchild);    //中序遍历右子树
	}
}
 
void visit(ThreadNode *q) {
    if(q->lchild==NULL){        //左子树为空,建立前驱线索
        q->lchild=pre;          //
        q->ltag=1;              //修改ltag=1,只有变成1才表示指针是线索
    }
    if(pre!=NULL&&pre->rchild==NULL){
        pre->rchild=q;          //建立前驱结点的后继线索
        pre->rtag=1;
    }
    pre=q;
}
 
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;           //pre没有前驱,最开始指向NULL
 
 
// 中序化线索二叉树T
void CreateInThread(ThreadTree T) {
    pre = NULL;                //pre初始化为NULL
	if (T != NULL) {           //非空二叉树才能线索化
		InThread(T, pre);      //中序化线索二叉树
		if(pre->rchild = NULL)
		    pre->rtag = 1;     //处理遍历的最后一个结点
	}
}

  • 先序线索化
// 先序遍历二叉树T
void PreThread(ThreadTree T) {
	if (T != NULL) {           //非空二叉树才能线索化
        visit(T);              //先处理根结点
		if(T->ltag ==0)        //lchild不是前驱线索
            PreThread(T->child);
        PreThread(T->child);
  	}
}
 
 
void visit(ThreadNode *q) {
    if(q->lchild==NULL){        //左子树为空,建立前驱线索
        q->lchild=pre;          //
        q->ltag=1;              //修改ltag=1,只有变成1才表示指针是线索
    }
    if(pre!=NULL&&pre->rchild==NULL){
        pre->rchild=q;          //建立前驱结点的后继线索
        pre->rtag=1;
    }
    pre=q;
}
 
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;           //pre没有前驱,最开始指向NULL
 
void CreateInThread(ThreadTree T) {
    pre = NULL;                //pre初始化为NULL
	if (T != NULL) {           //非空二叉树才能线索化
		PreThread(T);          //先序化线索二叉树
		if(pre->rchild = NULL)
		    pre->rtag = 1;     //处理遍历的最后一个结点
	}
}
  • 后序线索化
// 后序遍历二叉树T
void PostThread(ThreadTree T) {
	if (T != NULL) {           //非空二叉树才能线索化
		PreThread(T->child);   //后序遍历左子树
        PreThread(T->child);   //后序遍历右子树
        visit(T);              //访问根结点
  	}
}
 
 
void visit(ThreadNode *q) {
    if(q->lchild==NULL){        //左子树为空,建立前驱线索
        q->lchild=pre;          //
        q->ltag=1;              //修改ltag=1,只有变成1才表示指针是线索
    }
    if(pre!=NULL&&pre->rchild==NULL){
        pre->rchild=q;          //建立前驱结点的后继线索
        pre->rtag=1;
    }
    pre=q;
}
 
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;           //pre没有前驱,最开始指向NULL
 
//后续线索化二叉树T
void CreateInThread(ThreadTree T) {
    pre = NULL;                //pre初始化为NULL
	if (T != NULL) {           //非空二叉树才能线索化
		PostThread(T);          //后续线索化二叉树
		if(pre->rchild = NULL)
		    pre->rtag = 1;     //处理遍历的最后一个结点
	}
}

5.3.2_3 在线索二叉树中找前驱后继
  • 在中序线索二叉树中找到指定结点*p 的中序后继 next

①若 p->rtag==1,则 next = p->rchild

②若 p->rtag==0,则 next = p 的右子树中最左下结点

// 找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *FirstNode(ThreadNode *p){
    // 循环找到最左下结点(不一定是叶结点)
    while(p->ltag==0)
        p=p->rchild;
    return p;
}
 
// 在中序线索二叉树中找到结点p的后继结点
ThreadNode *NextNode(ThreadNode *p){
    // 右子树中最左下的结点
    if(p->rtag==0)
        return FirstNode(p->lchild);
    else
        return p->rchild;    //rtage==1直接返回后继线索
}
 
// 对中序线索二叉树进行中序循环(利用线索实现的非递归方法) 空间复杂度O(1)
void InOrder(ThreadNode *T){
    for(ThreadNode *p=FirstNode(T); p!=NULL; p=NextNode(p))
        visit(p);
}
  • 在中序线索二叉树中找到指定结点*p 的中序前驱 pre

①若 p->ltag==1,则 pre = p->lchild

②若 p->ltag==0

// 找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *LastNode(ThreadNode *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;        //ltage==1直接返回前驱线索
}
 
// 对中序线索二叉树进行中序循环(非递归方法实现)
void RevOrder(ThreadNode *T){
    for(ThreadNode *p=LastNode(T); p!=NULL; p=PreNode(p))
        visit(p);
}
  • 在先序线索二叉树中找到指定结点*p 的先序后继 next

①若 p->rtag==1,则 next = p->rchild

②若 p->rtag==0

  • 在先序线索二叉树中找到指定结点*p 的先序前驱 pre

①若 p->ltag==1,则 next = p->lchild

②若 p->ltag==0

  • 在后序线索二叉树中找到指定结点*p 的后序前驱 pre

①若 p->ltag==1,则 pre = p->lchild

②若 p->ltag==0

  • 在后序线索二叉树中找到指定结点*p 的后序后继 next

①若 p->rtag==1,则 next = p->rchild

②若 p->rtag==0

5.4 树和森林

5.4.1 树的存储结构
  • 树的存储1:双亲表示法 用数组顺序存储各结点,每个结点中保存数据元素、指向双亲结点(父结点)的“指针”
#define MAX_TREE_SIZE 100
 
// 树的结点
typedef struct{
    ElemType data;
    int parent;
}PTNode;
 
// 树的类型
typedef struct{
    PTNode nodes[MAX_TREE_SIZE];
    int n;	//结点数量
}PTree;

优点:找双亲(父结点)很方便

缺点:找孩子不方便,只能从头到尾遍历整个数组

  • 树的存储2:孩子表示法(顺序+链式存储)
#define MAX_TREE_SIZE 100
 
struct CTNode{
    int child;	//孩子结点在数组中的位置
    struct CTNode *next;	//下一个孩子
}
 
typedef struct{
    ElemType data;
    struct CTNode *firstChild;	//第一个孩子
}CTBox;
 
typedef struct{
    CTBox node[MAX_TREE_SIZE];
    int n,r;	//结点数和根的位置
}CTree;

优点:找孩子很方便

缺点:找双亲(父结点)不方便,只能遍历每个链表

  • 树的存储3:孩子兄弟表示法 树的孩子兄弟表示法与二叉树类似,采用二叉链表实现,每个结点内保存数据元素和两个指针,但两个指针的含义和二叉树结点不同
//孩子兄弟表示法结点
typedef struct CSNode{
    ElemType data;
    struct CSNode *firstchild, *nextsibling;	//第一个孩子和右兄弟结点
}CSNode, *CSTree;
 
5.4.2 树、森林与二叉树的转换
  • 树到二叉树的转换

①先在二叉树中,画一个根节点。

②按“树的层序”依次处理每个结点。

处理一个结点的方法是:如果当前处理的结点在树中有孩子,就把所有孩子结点 “用右指针串成糖葫芦”,并在二叉树中把第一个孩子挂在当前结点的左指针下方

  • 森林到二叉树的转换

①先把所有树的根结点画出来,在二叉树中用右指针串成糖葫芦。

②按“森林的层序”依次处理每个结点。

处理一个结点的方法是:如果当前处理的结点在树中有孩子,就把所有孩子结点“用右 指针串成糖葫芦”,并在二叉树中把第一个孩子挂在当前结点的左指针下方

注意:森林中各棵树的根节点视为平级的兄弟关系

  • 二叉树到树的转换

①先画出树的根节点

②从树的根节点开始,按“树的层序”恢复每个结点的孩子

如何恢复一个结点的孩子:在二叉树中,如果当前处理的结点有左孩子,就把左孩 子和“一整串右指针糖葫芦” 拆下来,按顺序挂在当前结点的下方

  • 二叉树到森林的转换

①先把二叉树的根节点和“一整串右指针糖葫芦”拆下来,作为多棵树的根节点

②按“森林的层序”恢复每个结点的孩子

如何恢复一个结点的孩子:在二叉树中,如果当前处理的结点有左孩子,就把左孩子和“一整串右指针糖葫 芦” 拆下来,按顺序挂在当前结点的下方

5.4.3 树和森林的遍历

树的先根遍历。若树非空,先访问根结点, 再依次对每棵子树进行先根遍历。(深度优先遍历)

void PreOrder(TreeNode *R){
    if(R!=NULL){
        visit(R);
        while(R还有下一个子树T)
            PreOrder(T);
    }
}

树的先根遍历序列与这棵树相应二叉树的先序序列相同

树的后根遍历。若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。(深度优先遍历)

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

  • 树的层次遍历(用队列实现):(广度优先遍历)

    ①若树非空,则根节点入队

    ②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队

    ③重复②直到队列为空

  • 森林的先序遍历 森林。森林是m (m>0)棵互不相交的树的集合。每棵树去掉根节点后,其各个子树又组成森林。

若森林为非空,则按如下规则进行遍历:

  • 访问森林中第一棵树的根结点。

先序遍历第一棵树中根结点的子树森林。

先序遍历除去第一棵树之后剩余的树构成的森林。

效果等同于依次对各个树进行先根遍历,等同于对二叉树的先序遍历。

  • 森林的中序遍历

中序遍历森林中第一棵树的根结点的子树森林。

访问第一棵树的根结点。

中序遍历除去第一棵树之后剩余的树构成的森林

效果等同于依次对各个树进行后根遍历,等同于对二叉树的中序遍历。

5.5 树与二叉树的应用

5.5.1 哈夫曼树

结点的权:有某种现实含义的数值。

结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积。

树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL,Weighted Path Length)

  • 哈夫曼树的定义:在含有 n 个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树

  • 哈夫曼树的构造

给定n个权值分别为w1, w2,..., wn的结点,构造哈夫曼树的算法描述如下:

  1. 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F.

  2. 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为

  3. 左、右子树上根结点的权值之和。

  4. 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。

  5. 重复步骤2)和3),直至F中只剩下一棵树为止。

  • 哈夫曼树的性质
  1. 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。

  2. 哈夫曼树的结点总数为 2n−1。

  3. 哈夫曼树中不存在度为 1 的结点。

  4. 哈夫曼树并不唯一,但 WPL 必然相同且为最优。

  • 哈夫曼编码

固定长度编码――每个字符用相等长度的二进制位表示

可变长度编码――允许对不同字符用不等长的二进制位表示

若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码

有哈夫曼树得到哈夫曼编码――字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树

哈夫曼编码可用于数据压缩

5.5.2_1 并查集

逻辑结构:数据元素之间为“集合”关系

  • 集合的两个基本操作——“并”和“查”

Find ——“查”操作:确定一个指定元 素所属集合

Union ——“并”操作:将两个不想交 的集合合并为一个

注:并查集(Disjoint Set)是逻辑结构——集合的一种具体实现,只进行 “并”和“查”两种基本操作 “并查集”的存储结构

“并查集”的代码实现——初始化

#define SIZE 13
int uFsets [ SIZE];    //集合元素数组
//初始化并查集
void Initial (int S[]){
    for(int i=0;i<SIZE;i++)
        s[i]=-1;
}

“并查集”的代码实现——并、查

//Find“查”操作,找x所属集合(返回x所属根结点)
int Find (int S[],int x){
    while(S[x]>=0)    //循环寻找x的根
        x=S[x] ;
    return x;         //根的s[]小于0
)
 
// union“并”操作,将两个集合合并为一个
void union(int S[],int Root1,int Root2){
    //要求Root1与Root2是不同的集合
    if(Root1==Root2)return;
    //将根Root2连接到另一根Root1下面
    S[Root2]=Root1;
  • Union操作的优化

优化思路:在每次Union操作构建树的时候,尽可能让树不长高

①用根节点的绝对值表示树的结点总数

②Union操作,让小树合并到大树

// Union“并”操作,小树合并到大树
void Union (int S[],int Root1,int Root2 ){
    if(Root1==Root2 ) return;
    if(S[Root2]>S[Root1]) { //Root2结点数更少
        S[Root1]+=S[Root2]; //累加结点总数
        S[Root2]=Root1;     //小树合并到大树
    }else{
        S[Root2]+=S[Root1]; //累加结点总数
        S[Root1]=Root2;     //小树合并到大树
    }
}
 
5.5.2_2 并查集的进一步优化

拓展:Find操作的优化(压缩路径)

先找到根结点,再将查找路径上所有结点都挂到根结点下

//Find“查"操作优化,先找到根节点,再进行“压缩路径”
int Find(int S[],int x){
    int root = x;
    while(S[root]>=0)  root=S[root]; //循环找到根
    while(x! =root){   //压缩路径
        int t=S[x];    //t指向x的父节点
        S[x]=root;     //x直接挂到根节点下
        x=t;
    }
return root;           //返回根节点编号
}