数据结构 - 第 6 章 树和二叉树

考纲内容

(一)树的基本概念

(二)二叉树;二叉树的定义及其主要特征;二叉树的顺序存储结构和链式存储结构;

           二叉树的遍历;线索二叉树的基本概念和构造

(三)树 、森林

           树的存储结构;森林与二叉树的转换;树和森林的遍历

(四)树与二叉树的应用

           二叉排序树;平衡二叉树;哈夫曼树和哈夫曼编码

知识框架

数据结构 - 第 6 章 树和二叉树_第1张图片

复习提示

  本章内容多以选择题的形式考查,但也会出涉及树遍历相关的算法题;树和二叉树的性质 、遍历操作 、转换 、存储结构和操作特性等,满二叉树 、完全二叉树 、线索二叉树 、哈夫曼树的定义和性质,二叉排序树和二叉平衡树的性质和操作等,都是选择题必然会涉及的内容

1. 树的基本概念

1.1 树的定义

树的定义
树的概念 树是 n( n\geq 0 )个结点的有限集
空树 当 n = 0 时,称为 "空树"
树的特点

在任意一棵非空树中,应满足:

1).  有且仅有一个特定的称为 "根" 的结点

2).  当 n > 1 时,其余结点可分为 mm > 0)个互不相交的有限集

T_{1} , T_{2} , ... , T_{m} ,其中每个集合本身又是一棵树,并且称为 "根" 的 "子树"

递归特性 显然,树的定义是递归的,即在树的定义中又用到了其自身,树是一种递归的数据结构;
树的逻辑特点

树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:

1).  树的根结点没有前驱,除根结点外的所有结点有且仅有一个前驱

2).  树中所有结点可以有零个或多个后继

总结

树,适合于表示具有层次结构的数据;

树中的某个结点(除根结点外)最多只和上一层的一个结点(即其父结点)有直接关系,根结点没有父结点:因此在 n 个结点的树种有 n - 1 条边

树种每个结点与其下一层的零个或多个结点(即其子结点)有直接关系

1.2 基本术语

基本术语
下面结合下图中的树来说明一些基本术语和概念
数据结构 - 第 6 章 树和二叉树_第2张图片
祖先

考虑结点 K ,根结点 A 到结点 K 的唯一路径上的任意结点,称为结点 K 的 "祖先" 

结点 E 、B 、A 都是结点 K 的祖先

子孙 与祖先的概念对应,结点 K 是结点 E 、B 、A 的 "子孙"
双亲

在通往树的根结点的路径上,最接近当前结点的祖先称为当前结点的 "双亲" 

结点 E 、B 、A 都是结点 K 的祖先,其中结点 E 最接近,所以结点 E 是结点 K 的双亲

根结点 A 是唯一没有双亲的结点

孩子 与双亲的概念对应,结点 K 是结点 E 的 "孩子"(直接相连的子结点)
兄弟

有相同双亲的结点称为 "兄弟" 

结点 K 和结点 L 有相同的双亲结点 E ,所以 K 和 L 是兄弟

堂兄弟

双亲在同一层的结点互为 "堂兄弟"

结点 G 与 E 、F 、H 、I 、J 互为堂兄弟

结点的度

树中一个结点的孩子个数称为该 "结点的度"

结点 B 的度为 2 ,结点 D 的度为 3

树的度

树种结点的最大度数称为 "树的度" 

取最大的结点的度,即为树的度

上面的树的度为 3

分支结点

度大于 0 的结点称为 "分支结点"(也称 非终端结点 )

在分支结点中,每个结点的分支数就是该结点的度

叶子结点 度等于 0(没有子结点)的结点称为 "叶子结点"(也称 终端结点)
结点的层次

"结点的层次" 从树根开始定义,根结点为第 1 层,其子结点为第 2 层,以此类推

结点的深度

"结点的深度" 是从根结点开始自顶向下逐层累加的;

根结点的深度为 1

结点的高度

"结点的高度" 是从叶子结点开始自底向上逐层累加的

叶子结点的高度为 1

树的高度 (深度)

树的高度(或深度)是树种结点的最大层数

上图中,树的高度(深度)为 4

有序树 树中结点的各子树从左到右是有次序的,不能互换,称该树为 "有序树"
无序树 不是有序树,即为无序树
路径 树中两个结点之间的 "路径" ,是由这两个结点之间所经过的结点序列构成的
路径长度 "路径长度" 是路径上所经过的边的条数
注意 由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上往下的,同一双亲的两个孩子之间不存在路径
森林

森林是 m( m\geq 0 )棵互不相交的树的集合

森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林

反之,只要给 m 棵独立的树加上一个结点,并把这 m 棵树作为该结点的子树,则森林就变成了树

1.3 树的性质

树的性质
1

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

解释:结点的度数就是结点的分支个数,所以一个度就是一条分支,在树中,除了根结点,每个结点都用一条分支与其父结点相连n 个结点的树中有 n-1 条分支(度数),也就是:树中的结点数等于所有结点的度数加 1
2

度为 m 的树中,第 i 层上至多有 m^{i-1} 个结点( i\geq 1

解释:m 叉树,等比数列,第一层结点数为 1 ,求第 m 层的结点个数

(等比数列,已知比值,第一项的值,求一般项的值)

3 高度为 h 的 m 叉树,至多有 (m^{h} - 1)/(m-1) 个结点
等比数列求和(已知比值 、项数,求和)
4 具有 n 个结点的 m 叉树的最小高度为 [ log_{m}(n(m-1) - 1) ]
每层都填满(已知比值 、和,求是第几项)

2. 二叉树的概念

2.1 二叉树的定义及其主要特性

二叉树的定义及其主要特性
二叉树的特征
1 二叉树是另一种树形结构
2 特点是每个结点至多只有两棵子树(即二叉树中不存在度大于 2 的结点)
3 二叉树的子树有左右之分,其次序不能任意颠倒
二叉树的定义
1 与树的定义相似,二叉树也以递归的形式定义
2 二叉树是 n( n\geq 0 )个结点的有限集合
3 或为空二叉树,即 n= 0
4

或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成;

左子树和右子树,也分别是一棵二叉树

二叉树是有序树
1 二叉树是有序树,若将其左 、右子树颠倒,则称为另一棵不同的二叉树
2 即使树中结点只有一棵子树,也要区分其是左子树还是右子树
二叉树的 5 种基本形态
数据结构 - 第 6 章 树和二叉树_第3张图片
二叉树与度为 2 的有序树的区别
1 度为 2 的树,至少有 3 个结点,但二叉树可以为空
2

度为 2 的有序树的孩子的左右次序是相对于另一孩子而言的,若某个结点只有一个孩子,则这个孩子就无须区分左右次序;

二叉树无论其孩子个数是否为 2 ,都需要确定其邹游次序,即二叉树的结点次序不是相对于另一结点而言,而是确定的

几类特殊的二叉树
满二叉树
定义

一棵高度为 h ,且含有 2^{h} - 1 个结点的二叉树,称为 "满二叉树"

即,树中的每层都含有最多的结点(每一层结点都排满,没有空缺)

特点

满二叉树的叶子结点都集中在二叉树的最下一层;

除叶子结点外的每个结点度数都是 2

结点编号

可以对满二叉树按层序编号:

约定编号从根结点(根结点编号为 1)开始,自上而下,从左往右,这样每个结点都对应一个编号

编号为 i 的结点,若有双亲,则其双亲的编号为 [ i/2 ] ;

若有左孩子,左孩子编号为 2i ;

若有右孩子,右孩子编号为 2i+1

完全二叉树
定义

高度为 h ,有 n 个结点的二叉树,当且仅当其中每个结点都与高度为 h 的满二叉树中编号为 1 ~ n 的结点一一对应时,称其为 "完全二叉树"

相当于在给满二叉树编号后,删除结点按照编号从大到小依次删除,从而得到完全二叉树

完全二叉树的特点
1

若 i\leq [n/2] ,则结点 i 为分支结点,否则为叶子结点;

满二叉树中有奇数个结点,叶子结点个数等于分支结点个数加一;在形成完全二叉树的过程中,删两个叶子结点才能得到一个新的叶子结点

2

叶子结点只可能在层次最大的两层上出现;

对于最大层次中的叶子结点,都依次排列在该层最左边的位置

3

若有度为 1 的结点,则只可能有一个,且该结点只有左孩子而没有右孩子 (重要特征)

从满二叉树可以看出,没有度为 1 的结点,结点的度要么为 2 ,要么为 0

编号从大到小删除结点,删一个结点,则该结点的父结点的度从 2 变为 1 ;删两个结点,则父结点的度从 2 变为 1

数据结构 - 第 6 章 树和二叉树_第4张图片
4

按层序编号后,一旦出现某结点(编号为 i )为叶子结点或只有左孩子,则编号大于 i 的结点均为叶子结点

从上图可以看出,叶子结点集中在最下两层,而且叶子结点个数和分支结点个数要么相等,要么多一个;以上图中编号为 4 的结点为例

5

若结点总数 n 为奇数,则每个分支结点都有左孩子和右孩子;

若结点总数 n 为偶数,则编号最大的分支结点(编号为 n/2 )只有左孩子,没有右孩子,其余分支结点 左 、右孩子都有

二叉排序树
1

左子树上的所有结点的关键字都小于(当前)根结点的关键字;

2 右子树上的所有结点的关键字都大于(当前)根结点的关键字;
3 左子树和右子树又各是一棵二叉排序树
平衡二叉树
1 树上任一结点的左子树和右子树的深度之差不超过 1
二叉树的性质

1

重要性质

非空二叉树上的叶子结点个数,等于度为 2 的结点个数加 1 ,即 n_{0} = n_{2} + 1

证明:设度为 0 、1 、2 的结点个数分别为 n_0n_1n_2

结点总数 n = n_0 + n_1 + n_2

设分支总数为 x ,由树的性质 1 可知, n = x + 1

由于这些分支是由度为 1 或 2 的结点发出的,所以 x = n_1 + 2n_2

n_0 + n_1 + n_2 = n_1 + 2n_2 + 1

所以,n_0 = n_2 + 1

仔细观察,下面的满二叉树和完全二叉树都符合该性质

数据结构 - 第 6 章 树和二叉树_第5张图片
2 非空二叉树上,第 k 层上至多有 2^{k-1} 个结点( k\geq 1
3 高度为 h 的二叉树,至多有 2^{h} - 1 个结点( h\geq 1

4

完全二叉树

性质

对完全二叉树按从上往下 、从左到右的顺序依次编号1、2、... 、n,则有以下关系:

(1). 当 i \geq 1 时,结点 i 的双亲(父结点)的编号为 [ i/2 ] ;

i 为偶数时,其双亲结点的编号为 i/2 ,是双亲结点的左孩子;

i 为奇数时,其双亲结点的编号为 (i - 1) / 2 ,是双亲结点的右孩子

(2). 当 2i \leq n 时,结点 i 的左孩子编号为 2i ,否则无左孩子(当 2i > n 时)

(3). 当 2i + 1 \leq n 时,结点 i 的右孩子编号为 2i+1 ,否则无右孩子

(当 2i+1> n 时)

(4). 结点 i 所在的层次(深度)为 [ log_{2}i ] + 1

5

具有 n 个(n> 0)结点的完全二叉树的高度为 [ log_{2}(n+1) ] 或

log_{2}n ] + 1

2.2 二叉树的存储结构

二叉树的存储结构
1. 顺序存储结构
顺序存储概念

二叉树的顺序存储是指,用一组地址连续的存储单元依次自上而下 、从左往右存储完全二叉树上的结点元素

即,将完全二叉树上编号为 i 的结点元素存储在一维数组下标为 i-1 的分量中

适用场景 依据二叉树的性质,完全二叉树和满二叉树,比较适合实用顺序存储
优点

i.   树中结点的序号可以唯一地反映结点之间的逻辑关系;

ii.  这样能最大限度地节省存储空间;

iii. 可以利用数组元素的下标值,确定结点在二叉树中的位置,以及结点之间的关系

一般二叉树

顺序存储

但对于一般的二叉树,为了让数组下标能反映二叉树中结点之间的逻辑关系,只能添加一些不存在的空结点,让这棵一般二叉树的每个结点与完全二叉树的结点相对照,再存储到一维数组的相应分量中;

在最坏情况下,一个高度为 h 且只有 h 个结点的单支树却需要占据近 2^{h} - 1 个存储单元

非完全二叉树的顺序存储结构如下图,可以在数组中用 0 表示实际不存在的空结点
数据结构 - 第 6 章 树和二叉树_第6张图片
注意

这种存储结构,建议从数组下标 1 开始存储树中的结点;

若从数组下标 0 开始存储,则不满足性质 4 的描述

2. 链式存储结构
顺序存储回顾

由于顺序存储的空间利用率较低,因此二叉树一般都采用链式存储结构

链式存储概念

所谓树的链式存储,即用链表结点来存储二叉树中的每个结点;

在二叉树中,结点结构通常包括若干数据域和若干指针域
二叉链表至少包含 3 个域 :数据域 data 、左指针域 lchild 、右指针域 rchild
所谓二叉链表,就是二叉树的链式存储结构
示例图

下图所示为常用的二叉链表的存储结构;

实际上,在不同的应用中,还可以增加某些指针域,如增加指向父结点的指针后,变为三叉链表的存储结构

二叉树

结点结构

二叉链表 数据结构 - 第 6 章 树和二叉树_第7张图片
结点类型描述 二叉树的链式存储结构描述如下:

typedef int ElemType;

typedef  struct  BTNode {

    ElemType data;                             // 数据域

    struct  BTNode  *lchild,*rchild;    // 左 、右孩子指针

} BTNode ,*BTree;

选择合适

存储结构

使用不同的存储结构时,实现二叉树操作的算法也会不同,因此要根据实际应用场合(二叉树的形态和需要进行的运算)来选择合适的存储结构
重要结论 在含有 n 个结点的二叉链表中,含有 n+1 个空链域
推导

链域总数为两倍的结点个数 x = 2n(每个结点含有 lchild 和 rchild)

非空链域个数等于分支个数 n - 1 (除根结点,每个结点都用一根分支指向其父结点)

空链域个数 = 链域总数 - 非空链域个数 = 2n - (n - 1) = n + 1

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

3.1 二叉树的遍历

二叉树的遍历

二叉树遍历

概念

二叉树的遍历是指,按某条搜索路径访问树中的每一个结点,使得每个结点都被访问一次,且仅被访问一次
遍历的思路 由于二叉树是一种非线性结构,每个结点都可能有两棵子树,因而需要寻找一种规律,使得二叉树上的结点能排列在一个线性队列中,进而更容易遍历
三种遍历方式

由二叉树的递归定义可知,遍历一棵二叉树需要决定对跟结点 N 、左子树 L 、右子树 R 的访问顺序;

按照先遍历左子树,再遍历右子树的原则,常见的遍历次序有 :

1). 先序遍历  NLR

2). 中序遍历  LNR

3). 后序遍历  LRN

其中, "序" 指的是根结点在何时被访问

数据结构 - 第 6 章 树和二叉树_第8张图片
1. 先序遍历
先序遍历 先序遍历(PreOrder)的操作过程如下:

若二叉树为空,则什么也不做;否则

1). 访问根结点

2). 先序遍历左子树

3). 先序遍历右子树

对应的递归算法如下:

void PreOrder(BTree T) {

    if (T != NULL) {

        visit(T);                        // 访问根结点

        PreOrder(T->lchild);    // 递归遍历左子树

        PreOrder(T->rchild);    // 递归遍历右子树

    }

}

对于上图中的二叉树,先序遍历所得到的结点序列为 : 1  2  4  6  3  5
2. 中序遍历
中序遍历 中序遍历(InOrder)的操作过程如下:

若二叉树为空,则什么也不做;否则

1). 中序遍历左子树

2). 访问根结点

3). 中序遍历右子树

对应的递归算法如下:

void InOrder(BTree T) {

    if (T != NULL) {

        InOrder(T->lchild);       // 递归遍历左子树

        visit(T);                        // 访问根结点

        InOrder(T->rchild);      // 递归遍历右子树

    }

}

对于上图中的二叉树,中序遍历所得到的结点序列为 : 2  4  6  1  3  5
3. 后序遍历
后序遍历(PostOrder)的操作过程如下:

若二叉树为空,则什么也不做;否则

1). 后序遍历左子树

2). 后序遍历右子树

3). 访问根结点

对应的递归算法如下:

void PostOrder(BTree T) {

    if (T != NULL) {

        PostOrder(T->lchild);       // 递归遍历左子树

        PostOrder(T->rchild);      // 递归遍历右子树

        visit(T);                            // 访问根结点

    }

}

对于上图中的二叉树,后序遍历所得到的结点序列为 : 6  4  2  5  3  1
总结 三种遍历算法中,递归遍历左 、右子树的顺序是固定的(总是先递归遍历左子树,再递归遍历右子树),只是访问根结点的顺序不同
不管采用哪种遍历算法,每个结点都访问一次且仅访问一次,所以时间复杂度为 O(n)
在递归遍历中,递归工作栈的栈深度恰好为树的深度,所以在最坏情况下,二叉树是具有 n 个结点且深度为 n 的单支树,遍历算法的空间复杂度为 O(n)
注意 以上三种遍历方式及算法描述是简单易懂的,需要将其作为模板来记忆,考研中的很多题目都是基于这 3 个模板延伸出来的
4. 递归算法和非递归算法的转换

在上面介绍的 3 种遍历算法中,暂时删除和递归无关的 visit 语句,则 3 个遍历算法完全相同;

因此,从递归执行过程的角度来看,先序 、中序 、后序,这三种遍历是完全相同的

下图中,用带箭头的虚线表示了这 3 种遍历算法的递归执行过程;

其中,向下的箭头表示更深一层的递归调用,向上的箭头表示从递归调用退出返回;

虚线旁的三角形 、圆形 、方形内的字符分别表示在先序 、中序 、后序遍历过程中访问结点时输出的信息

例如,由于中序遍历中,访问结点操作在遍历左子树之后,在遍历右子树之前,则带圆形的字符标在向左递归返回和向右递归调用之间;

只要沿虚线从 1 出发到 2 结束,将沿途所见的三角形(或圆形 、或方形)内的字符记下,便能得到遍历二叉树的先序(或中序或后序)序列

例如,在下图中,沿虚线游走可以分别得到:

先序序列  A  B  D  E  C

中序序列  D  B  E  A  C

后序序列  D  E  B  C  A

数据结构 - 第 6 章 树和二叉树_第9张图片
中序遍历过程分析
借助栈,我们来分析中序遍历的访问过程:
步骤一

沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点,

此时栈内元素从栈底到栈顶依次为  A  B  D

步骤二

栈顶元素(根结点)出栈并访问;

若其右孩子为空,则继续执行步骤二

步骤三 若其右孩子不为空,则让右子树执行步骤一
具体分析

栈顶元素 D 出栈并访问,D 是中序序列的第一个结点;

D 的右孩子为空,当前的栈顶元素 B 出栈并访问;

B 的右孩子不为空,将 B 的右孩子 E 入栈;

E 的左孩子为空,栈顶元素 E 出栈并访问;

E 的右孩子为空,栈顶元素 A 出栈并访问;

A 的右孩子不为空,将 A 的右孩子 C 入栈;

C 的左孩子为空,栈顶元素 C 出栈并访问;

由此,得到中序序列 D B E A C

根据分析,可以写出中序遍历的非递归算法:

void InOrder2(BTree T) {

    InitStack(S);

    BTree pT = T;

    while (pT != NULL ||!IsEmpty(S))

    {

        if (pT)

        {

            Push(S, pT);

            pT = pT->lchild;

        }

        else

        {

            Pop(S, pT);

            visit(pT);

            pT = pT->rchild;

        }

    }

}

先序遍历过程分析
先序遍历和中序遍历的基本思想是类似的,只需把访问结点操作放在入栈操作的前面

void PreOrder2(BTree T) {

    InitStack(S);

    BTree pT = T;

    while (pT != NULL ||!IsEmpty(S))

    {

        if (pT)

        {

            visit(pT);

            Push(S, pT);

            pT = pT->lchild;

        }

        else

        {

            Pop(S, pT);

            pT = pT->rchild;

        }

    }

}

后序遍历过程分析
难点

后序遍历的非递归实现,在三种遍历方法中,是最难的;

难点在于,在后序遍历中,要保证左孩子和右孩子都已被访问并且左孩子在右孩子前面被访问,才能访问根结点,这就为流程控制带来了难题

思路分析

从根结点开始,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点;

此时不能出栈并访问,因为如果其有右子树,还需按相同的规则对其右子树进行处理;

直至上述操作进行不下去,若栈顶元素想要出栈并访问,要么右子树为空,要么右子树刚被访问完(此时左子树早已被访问完)

void PostOrder2(BiTree T)

{
        InitStack(S);
        p = T;
        r = NULL;


        while ( p || !IsEmpty(S) )

        {
                if(p)        //走到最左边

                {    
                        push(S, p);
                        p = p->lchild;
                }

                else        //向右

                {    
                        GetTop(S, p);    //读栈顶元素 (非出栈)


                        //若右子树存在,且未被访问过
                        if ( p->rchild && p->rchild != r )

                        {
                                p = p->rchild;       //转向右
                                push(S, p);          //压入栈
                                p = p->lchild;       //再走到最左
                        }

                        else                             //否则,弹出结点并访问

                        {    
                                pop(S, p);            //将结点弹出
                                visit(p->data);      //访问该结点
                                r = p;                   //记录最近访问过的结点
                                p = NULL;
                        }
                }
        }
}

5. 层序遍历
层序遍历含义 下图所示,二叉树的层序遍历,即按照箭头所指方向,按照 1 、2 、3 、4 的层次顺序,对二叉树中的各个结点进行访问
数据结构 - 第 6 章 树和二叉树_第10张图片
分析 要进行层序遍历,需要借助一个队列

步骤一:  先将二叉树的根结点入队,然后出队,访问出队结点

步骤二:  当前访问的结点,若它有左子树,则将左子树的根结点入队;若它有右子树,则将右子树的根结点入队;然后出队,访问出队结点

步骤三:  每次访问出队结点,都对该结点执行步骤二

如此反复,直至队列为空

void LevelOrder(BTree T)

{

        InitQueue(Q);

        BTree 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);

        }

}

Tips 上述二叉树层序遍历的算法,在复习过程中,应将其作为一个模板
注意

遍历,是二叉树各种操作的基础,可以在遍历的过程中对结点进行各种操作;

例如,对于一棵已知树求结点的双亲 、求结点的孩子结点 、求二叉树的深度 、求二叉树的叶子结点个数 、判断两棵二叉树是否相等;

所有这些操作都建立在二叉树遍历的基础上,因此必须掌握二叉树的各种遍历过程,并能灵活运用来解决各种问题

6. 由遍历序列构造二叉树

先 、中序列

确定二叉树

二叉树的先序序列和中序序列,可以唯一地确定一棵二叉树

在先序遍历序列中,第一个结点一定是二叉树的根结点;

在中序遍历序列中,根结点必然将中序序列分割成两个子序列;

前一个子序列是根结点的左子树的中序序列;

后一个子序列是根结点的右子树的中序序列;

根据这两个子序列,在先序序列中找到对应的左子序列和右子序列;

左子序列的第一个结点是左子树的根结点;

右子序列的第一个结点是右子树的根结点;

如此递归地进行下去,便能唯一地确定这棵二叉树

后 、中序列

确定二叉树

二叉树的后序序列和中序序列,可以唯一地确定一棵二叉树
因为后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似的方法递归地进行划分,从而得到一棵二叉树

层 、中序列

确定二叉树

二叉树的层序序列和中序序列,可以唯一地确定一棵二叉树
注意 若只知道二叉树的先序序列和后序序列,则无法唯一确定一棵二叉树

3.2 线索二叉树

线索二叉树
1. 线索二叉树的基本概念
遍历二叉树是以一定的规则,将二叉树中的结点排列成一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(第一个结点和最后一个结点除外)都有一个直接前驱和直接后继
传统的二叉链表存储,仅能体现一种父子关系,不能直接得到结点在遍历过程中前驱和后继

之前提到,在含有 n 个结点的二叉树中,有 n+1 个空指针(空链域);

每个叶子结点有 2 个空指针,每个度为 1 的结点有 1 个空指针,

空指针总数为  2n_0 + n_1

根据二叉树的性质一 : n_0 = n_2 + 1

所以空指针总数为  n_0 + n_1 + n_2 + 1 = n + 1

由此设想,能否利用这些空指针来存放指向其前驱或后继的指针?

这样就可以像遍历单链表那样方便地遍历二叉树

引入线索二叉树就是为了加快查找结点前驱和后继的速度

规定 若无左子树,令 lchild 指向其前驱结点
若无右子树,令 rchild 指向其后继结点

还需增加两个标志域;

标识 lchild 指向左子树还是前驱;

标识 rchild 指向右子树还是后继;

线索二叉树

结点结构

标志域的含义如下:
ltag 0 ,   lchild 域指示结点的左孩子
1 ,   lchild 域指示结点的前驱
rtag 0 ,   rchild 域指示结点的右孩子
1 ,   rchild 域指示结点的后继
结构类型描述 线索二叉树的存储结构描述如下:

typedef  struct  ThreadNode {

        ElemType  data;

        struct  ThreadNode *lchild,*rchild;

        int  ltag,rtag;

} ThreadNode,*ThreadTree;

总结

以这种结点结构构成的二叉链表作为二叉树的存储结构,称为 " 线索链表 " ;

其中指向结点前驱和后继的指针,称为 " 线索 " ;

加上线索的二叉树,称为 " 线索二叉树 "

2. 中序线索二叉树的构造

二叉树的线索化,是将二叉链表中的空指针,改为指向前驱或后继的线索

前驱或后继的信息,只有在遍历时才能得到因此线索化的实质就是遍历一次二叉树

图示说明

线索化过程

以中序线索二叉树的建立为例
附设指针 pre 指向刚刚访问过的结点指针 p 指向正在访问的结点,即 pre 指向 p 的前驱;在中序遍历的过程中,检查 p 的左指针是否为空,若为空就将它指向 pre检查 pre 的右指针是否为空,若为空就将它指向 p
数据结构 - 第 6 章 树和二叉树_第11张图片

中序遍历

线索化二叉树

通过中序遍历,对二叉树线索化的递归算法如下:

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);        //递归,线索化右子树
        }
}

中序遍历构造线索二叉树

通过中序遍历,建立中序线索二叉树的主过程算法如下:

void CreateInThread (ThreadTree T)

{

        ThreadTree pre  =  NULL;

        if (T != NULL)

        {

                InThread (T , pre);

                pre->rchild = NULL;

                pre->rtag = 1;

        }

}

带头结点

线索二叉树

为了方便,可以在二叉树的线索链表上,也添加一个头结点,令头结点的 lchild 域的指针指向二叉树的根结点,头结点的 rchild 域的指针指向中序遍历时访问的最后一个结点 :令二叉树中序序列中的第一个结点的 lchild 域指针和最后一个结点的 rchild 域指针,都指向头结点

这好比为二叉树建立了一个双向线索链表,方便从前往后或从后往前对线索二叉树进行遍历 

数据结构 - 第 6 章 树和二叉树_第12张图片
3. 中序线索二叉树的遍历
分析

中序线索二叉树的结点中,隐含了线索二叉树的前驱和后继信息;

在对其进行遍历时,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空;

在中序线索二叉树中,找结点后继的规律是:若其 rtag 为 1 ,则 rchild 为线索 ,指示其后继 ,否则(rtag 为 0 )遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继;

不含头结点的线索二叉树的遍历算法如下:

1).  求中序线索二叉树中中序序列下的第一个结点 :

ThreadNode  *FirstNode (ThreadNode *p)

{

        while ( p->ltag == 0 )

                p = p->lchild;

        return p;

}

2). 求中序线索二叉树中结点 p 在中序序列下的后继 :

ThreadNode  *NextNode (ThreadNode  *p)

{

        if ( p->rtag == 0 )

                return  FirstNode (p->rchild);

        else

                return  p->rchild;

}

3). 利用上面两个算法,可以写出不含头结点的中序线索二叉树的中序遍历算法

void InOrder (ThreadNode *T)

{

    for ( ThreadNode *p = FirstNode (T) ; p != NULL ; p = NextNode(p) )

    {

            visit(p);

    }

}

4. 先序线索二叉树和后序线索二叉树
上面给出了建立中序线索二叉树的代码,建立先序线索二叉树和后序线索二叉树的代码类似,只需要变动线索化改造的代码字段与调用线索化左右子树递归函数的位置
手动求先序线索二叉树
数据结构 - 第 6 章 树和二叉树_第13张图片

4. 树 、森林

4.1 树的存储结构

4.2 树 、森林与二叉树的转换

4.3 树和森林的遍历

4.4 树的应用 - 并查集

5. 树与二叉树的应用

5.1 二叉排序树( BST )

5.2 平衡二叉树

5.3 哈夫曼树和哈夫曼编码

你可能感兴趣的:(计算机考研,408,统考科目,数据结构)