目录
树和二叉树的定义
树结构是一类重要的非线性数据结构。
树的定义
树是n(n >= 0)个节点的有限集,它或为空树(n == 0),或为非空树。
对于非空树:
1.有且仅有一个称之为根的节点
2.除根结点意外的其余节点可分为m(m > 0)个互不相交的有限集T1、T2、T3...Tn,其中每一个集合本身又是一棵树,并且称为根的子树
树的结构定义是一个递归的定义,即在树的定义中又用到树的定义。它道出了树的固有特性。树还有其他的表示方式,如:嵌套集合、广义表、凹入表示法
树的基本术语
1.节点:书中的一个独立的单元,包含一个数据元素及若干个指向其子树的分支
2.节点的度:节点拥有的子树数称为节点的度
3.树的度:树的度是树内各节点度的最大值
4.叶子:度为0的节点称为叶子或终端节点
5.非终端节点:度不为0的节点称为非终端节点或分支节点。除根结点之外,非终端节点也称为内部节点
6.双亲和节点:节点的子树的根称为该节点的孩子,相应地,该节点称为孩子的双亲
7.兄弟:同一个双亲的孩子之前互称兄弟
8.祖先:从根到该节点所经分支上的所有节点
9.子孙:以某节点为根的子树中的任意节点都称为该节点的子孙
10.层次:节点的层次从根开始定义,根为第一层,根的孩子为第二层。书中任意节点的层次等于其双亲结点的层次加一
11.堂兄弟:双亲在同一层的节点互为堂兄弟
12.树的深度:树中节点的最大层次称为树的深度或者高度
13.有序树和无序树:如果将树中节点各子树堪称从左至右是有次序的(不能互换),则称该数为有序树,否则为无序树。在有序树当中最左边的子树的根称为第一个孩子,最右边的孩子称为最后一个孩子
14.森林:m(m >= 0)棵互不相交的树的集合,对于树中每个节点而言,其子树的集合即为森林
就逻辑结构而言,任何一颗树都是一个二元组Tree = (root,F),其中root是数据元素,称作树的根节点;F是包含m(m >= 0)棵树的森林,F = (T1,T2,...Tn),其中Ti = (r1,Fi)称作根root的第i棵子树;当m != 0时,在树根和其子树森林之间存在下列关系
这个定义将有助于得到森林和树与二叉树之间转换的递归定义
二叉树的定义
二叉树是n(n >= 0)个节点所构成的集合,它或为空树(n == 0),或为非空树(n > 0)。对非空树T:
1.有且仅有一个称之为根的节点
2.除根节点以外的其余节点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树
二叉树与树一样具有递归的性质,二叉树与树的区别主要有以下两点:
1.二叉树每个节点至多只有两棵子树(二叉树中不存在度大于2的节点)
2.二叉树的子树有左右之分,其次徐不能任意颠倒
二叉树的递归定义表示二叉树或为空,或由一个根节点加上两颗分别称为左子树和右子树的,互不相交的二叉树组成。由于这两棵树也是二叉树,则由二叉树的定义,它们也可以是空树,所以也就诞生了5中二叉树的基本形态:
空二叉树,仅有根节点的二叉树,右子树为空的二叉树,左子树为空的二叉树,左右节点均非空的二叉树
二叉树的性质和存储结构
二叉树的性质
二叉树具有以下重要性质
性质一:在二叉树的第i层上至多有(i >= 1)个节点
证明:利用归纳法容易证明此结论
i = 1时,只有一个根节点,显然是对的
现在假定所有的j(1 <= j < i)命题成立,即第j层上至多有个节点,那么可以证明j = i时也成立
由归纳假设,第i - 1层上至多有个节点,由于二叉树特性可得:每个节点的度至多为2,故在第i层上的最大节点数为在第i - 1层的最大节点数的二倍,也就是
性质二:深度为k的二叉树最多有(k >= 1)个节点
性质三:对任何一棵二叉树T,如果其终端节点数为n0,度为2的节点数为n2,则n0 = n2 + 1
下面介绍两种特殊形态的二叉树:满二叉树和完全二叉树
满二叉树:深度为k且含有个节点的二叉树
满二叉树的特点是:每一层上的节点数都是最大节点数,即每一层i的节点数都具有最大值
可以对满二叉树的节点进行连续编号,约定编号从根节点起,自上而下,从左至右。由此可以引出完全二叉树的定义
完全二叉树:深度为k的,有n个节点的二叉树,当且进档器每一个节点都与深度为k的满二叉树中编号从1至n的节点一一对应时,称之为完全二叉树
完全二叉树的特点是:
1.叶子节点只可能在层次最大的两层上出现
2.对任一节点,若其右分支下的子孙的最大层次为l,则其左分支下的子孙的最大层次必为l或l + 1
性质四:具有n个节点的完全二叉树的深度为
表示不大于x的最大整数,反之,也可表示为不小于x的最小整数
性质五:如果对一棵有n个节点的完全二叉树(其深度为)的节点按层序编号(从第1层到第层,每层从左到右),则对任意节点i(1 <= i <= n),以下结论成立
1.如果i = 1,则节点i是二叉树的根,无双亲;如果i > 1,则其双亲PARENT(i)是节点(i / 2)
2.如果2i > n,则节点i无左孩子(节点i为叶子节点),否则其左孩子LCHILD(i)是节点(2i)
3.如果2i + 1 > n,则节点无右孩子;否则其有孩子RCHILD(i)是节点(2i + 1)
二叉树的存储结构
顺序存储结构
#define MAXSIZE 100
typedef TELemType SqBiTree[MAXSIZE];
SqBiTree bt;
顺序存储结构使用一组地址连续的存储单元来存储数据元素,为了能够在存储结构中反映出节点之间的逻辑关系,必须将二叉树中的节 依照一定的规律安排在这组单元中
对于完全二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储节点元素,即将完全二叉树上编号为i的节点元素存储在如上定义的一维数组中下标为i - 1的分量中
对于一般二叉树,则应将其每个节点与完全二叉树上的节点相对照,存储在一维数组的相应分量中
由此可见,这种顺序存储结构仅适用于完全二叉树,因为在最坏的情况下,一个深度为k且只有k个节点的单支树(树中不存在度为2的节点)却需要长度为的一维数组,这造成了存储空间的极大浪费,因此对于一般二叉树,更适合采取链式存储结构
链式存储结构
设计不同的节点结构可构成不同形式的链式存储结构。由二叉树定义可知,二叉树的节点由一个数据元素和分别指向其左、右子树两个分支构成,则表示二叉树的链表中的节点至少包含三个域:数据域和左、右指针域。又是,为了便于找到节点的双亲,还可在节点结构中增加一个指向其双亲节点的指针域。利用这两种结构所得的二叉树的存储结构分别称为二叉链表和三叉链表。链表的头指针指向二叉树的根节点。容易证得,在含有n个节点的二叉链表中有n + 1个空链域
在不同的存储结构中,实现二叉树的操作方法也不同,例如找节点x的双亲PARENT(T,e),在三叉链表中很容易实现,而在二叉链表中则需从根指针出发巡查。由此,在具体应用中采用什么存储结构,除考虑二叉树的形态之外还应考虑需进行哪儿种操作。
typedef struct BiNode
{
TElemTpye data;
struct BiNode *lchild,*rchild;
}BiNode,*BiTree;
遍历二叉树和线索二叉树
遍历二叉树
遍历二叉树是指按某条搜索路径寻访树中的每个节点,使得每个节点被访问依次,而且仅被访问依次。访问的含义很广,可以是对节点进行各种处理,包括输出节点的信息,对节点进行运算和修改等。遍历二叉树是二叉树最基本的操作,也是二叉树其他各种操作的基础。遍历的是指是对二叉树进行线性化,即遍历的结果是讲非线性结构树中的节点排成一个线性序列。由于二叉树的每个节点都有可能有两颗子树,因此需要找寻一种规律,一遍是二叉树上的节点能排列在一个线性队列上,从而便于遍历
回顾二叉树的递归定义可知,二叉树由三个基本单元组成:根节点,左子树,右子树。因此,若能依次遍历这三部分,便是遍历了整个二叉树,假设用LDR分别表示左子树、访问根节点和右子树,那么可有LDR、LRD、DLR、DRL、RLD、RDL六中国遍历二叉树的方案,若限定先左后右,则只剩三种情况:DLR、LDR、LRD,我们称之为:先序遍历、中序遍历、后序遍历。基于二叉树的递归定义,可得下述遍历二叉树的递归算法定义:
先序遍历
先序遍历二叉树的操作定义如下:
若二叉树为空,则操作为空,否则:
1.访问根节点
2.先序遍历左子树
3.先序遍历右子树
先序遍历5.5二叉树得:-+a*b-cd/ef
中序遍历
中序遍历二叉树的操作定义如下:
若二叉树为空,则操作为空,否则:
1.中序遍历左子树
2.访问根节点
3.中序遍历右子树
中序遍历5.5二叉树得:a+b*c-d-e/f
后序遍历
后序遍历二叉树的操作定义如下:
若二叉树为空,则操作为空,否则:
1.后序遍历左子树
2.后序遍历右子树
3.访问根节点
后序遍历5.5二叉树得:abcd-*+ef/-
前序遍历的递归算法
void PreOrderTraverse(BiTree T)
{
if(T)
{
cout << T -> data;
PreOrderTraverse(T -> lchild);
PreOrderTraverse(T -> rchild);
}
}
中序遍历的递归算法
void InOrderTraverse(BiTree T)
{
if(T)
{
InOrderTraverse(T -> lchild);
cout << T -> data;
InOrderTraverse(T -> rchild);
}
}
后序遍历的递归算法
void PoOrderTraverse(BiTree T)
{
if(T)
{
PoOrderTraverse(T -> lchild);
PoOrderTraverse(T -> rchild);
cout << T -> data;
}
}
也可利用我们之前学过的栈来将递归算法改为非递归算法
1.工作记录中包含两项,其一是递归调用的语句编号,其二是指向根节点的指针,则当栈顶记录中的指针非空时,应遍历左子树,即左子树根的指针进栈
2.若栈顶记录中的指针值为空,则应退至上一层,若是从左子树返回,则应当访问当前层(栈顶记录)中指针所指的根节点
3.若是从右子树返回,则表明当前层的遍历结束,应继续退栈。从另一个角度看,这意味着遍历右子树是不再需要保存当前层的根指针,直接修改栈顶记录中的指针即可
现给出中序遍历的非递归算法
void InOrderTreaverse(BiTree T)
{
InitStack(S);
BiNode p = T,q = new BiNode;
while(p || !StackEmpty(S))
{
if(p)
{
Push(S,p);
p = p -> lchild;
}
else
{
Pop(S,q);
cout << q -> data;
p = q -> rchild;
}
}
}
无论是递归还是非递归遍历二叉树,因为每个节点被访问依次,则不论按哪儿一种次序进行遍历,对含n个节点的二叉树,其时间复杂度为O(n)。所需辅助空间为遍历过程中栈的最大容量,即树的深度,最坏情况下为n,则空间复杂度也为O(n)
二叉树的先序、中序、后续遍历是最常用的3种遍历方式。此外,还有一种按层次遍历二叉树的方式,这种方式按照“从上到下,从左到右”的顺序遍历二叉树,即先遍历二叉树第一层的节点,然后是第二层的节点,直至最底层的节点,对每一层的遍历按照从左到右的次序进行。层序遍历不是一个递归过程,层序遍历算法的实现可以借助队列这种数据结构
根据遍历确定二叉树
从前面讨论二叉树的比哪里可知,二叉树中各节点值均不相同,任意一棵二叉树节点的先序遍历、中序遍历、后序遍历都是唯一的,反过来,若已知二叉树遍历的任意两种序列,就可以确定这颗二叉树
先序遍历、中序遍历确定二叉树
根据定义,二叉树的先序遍历是访问先访问根节点,其次按先序遍历的方式遍历根节点的左子树,最后按先序遍历方式遍历根节点的右子树。这就是说,在先序序列中,第一个节点一定是二叉树的根节点。另一方面,中序遍历是先便利左子树而后访问根节点,最后遍历右子树。这样,根节点再中序序列中必然将中序序列分割成两个子序列,前一个子序列是根节点的左子树的中序序列,而后一个子序列是根节点的右子树的中序序列。根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中,左子序列的第一个节点是左子树的根节点,右子序列的第一个节点是右子树的根节点,这样,就确定了二叉树的三个节点。同时,左子树和右子树的根节点又可以把做子序列和右子序列划分为两个子序列,如此递归下去,当取尽先序序列中的节点时,便可得到一棵二叉树
中序遍历、后序遍历确定二叉树
同理,由二叉树的后序序列和中序序列也可唯一的确定一棵二叉树。因为,依据后序遍历和中序遍历的定义,后序遍历的最后一个节点就如同先序序列的第一个节点一样,可将中序序列分成两个子序列,分别为这个节点左子树的中序序列和右子树的中序序列,再拿出后序序列的倒数第二个节点,并继续分割中序序列,如此递归下去,当倒着取尽后序序列中的节点时,便可以得到一棵二叉树
先序序列、后序序列确定二叉树
打咩,仅由先序序列和后序序列是无法确定左右子树两部分的
二叉树遍历算法的应用
“遍历”是二叉树各种操作的基础,假设访问节点的具体操作不仅仅局限于输出节点数据域的值,而把“访问”延伸到对节点的判别、计数等其他操作,可以解决一些关于二叉树的其他实际问题。如果在遍历过程中生成节点,这样便可建立二叉树的存储结构
创建二叉树的存储结构-二叉链表
为简化问题,设二叉树中节点的元素均为单赐福。假设按先序遍历的顺序建立二叉链表,T为指向根节点的指针,对于给定的一个字符序列,依次读入字符,从根节点开始,递归创建二叉树
算法步骤
1.查找字符序列,读入字符ch
2.如果ch是一个'#'字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作
申请一个节点空间T
将ch赋给T -> data
递归创建T的左子树
递归创建T的右子树
算法描述
//按照先序遍历顺序创建二叉链表
void CreateBiTree(BiTree &T)
{
char ch;
cin >> ch;
if(ch == '#')T = NULL;
else
{
T = new BiNode;
ch = T -> data;
CreateBiTree(T -> lchild);
CreateBiTree(T -> rchild);
}
}
复制二叉树
复制二叉树就是利用以后的一棵二叉树复制得到另外一棵与其完全相同的二叉树。根据二叉树的特点,复制步骤如下:若二叉树不为空,首先复制根节点,这些相当于二叉树先序遍历算法中访问根节点的语句;然后分别复制二叉树根节点的左子树和右子树,这相当于先序遍历中递归遍历左子树和右子树的语句,因此,复制函数的实现与二叉树先序遍历的实现非常类似
算法步骤
如果是空树,递归结束,否则执行以下操作:
申请一个新节点,复制根节点
递归复制左子树
递归复制右子树
算法描述
//复制二叉树
void Copy(BiTree T,BiTree NewT)
{
if(T == NULL)
{
NewT = NULL;
return;
}
else
{
NewT = new BiNode;
NewT -> data = T -> data;
Copy(T -> lchild,NewT -> lchild);
Copy(T -> rchild,NewT -> rchild);
}
}
计算二叉树的深度
二叉树的深度为树中节点的最大层次,二叉树的深度为左右子树深度的较大者加一
算法步骤
如果是空树,递归结束,深度为0,否则执行以下操作:
递归计算左子树的深度记为m
递归计算右子树的深度记为m
如果m大于n,二叉树的深度为m + 1,否则为n + 1
算法描述
//计算二叉树的深度
int Depth(BiTree T)
{
if(T == NULL)return 0;
else
{
m = Depth(T -> lchild);
n = Depth(T -> rchild);
if(m > n)return (m + 1);
else return (n + 1);
}
}
统计二叉树中的节点个数
如果是空树,则结点个数为0,递归结束;否则,结点个数为左子树的结点个数加上右子树的结点个数再加上1
算法描述
//计算二叉树中的节点个数
int NodeCount(BiTree T)
{
if(T == NULL)return 0;
else return (NodeCount(T -> lchild) + NodeCount(T -> rchild) + 1)
}
线索二叉树
线索二叉树的基本概念
遍历二叉树是以一定规则将二叉树中的节点排列成一个线性排列,得到二叉树中节点的先序序列、中序序列和后序序列。这实质上是对一个非线性结构进行线性化操作,使每个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继
但是,当以二叉链表作存储结构时,只能找到节点的左右孩子信息,而不能直接得到节点在任一序列中的前驱和后继信息,这种信息只有在遍历的动态过程中才能得到,为此引入线索二叉树来保存这些在动态过程之中得到的有关前驱和后继的信息
虽然可以在每个节点中增加两个指针来存放在遍历时得到的有关前驱和后继信息,但这样做使得结构的存储密度大大降低。由于有n个节点的二叉链表必然存在n + 1个空链域,因此可以充分利用这些空链域来存放节点的前驱和后继信息
试进行如下规定:若节点有左子树,则其lchild域指示其左孩子,否则令lchild域指示其前驱,若节点有右子树,则其rchild指示其有孩子,否则令rchild域指示其后继,为了避免混淆,尚需改变节点结构,增加两个标志域LTag和RTag,其中:
LTag = 0 lchild域指示节点的左孩子 LTag = 1 lchild域指示节点的前驱
RTag = 0 rchild域指示节点的右孩子 RTag = 1 rchild域指示节点的后继
//二叉树的二叉线索存储表示
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild,*rchild;
int LTag,RTag;
}BiThrNode,*BiThrTree;
以这种节点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向节点前驱和后继的指针叫做线索。加上线索的二叉树称之为线索二叉树。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化
构造线索二叉树
由于线索二叉树构造的实质是将二叉链表中的空指针改为指向前驱或后继的线索,而前驱或后继的信息只有在遍历时才能够得到,因此线索化的过程即在遍历的过程中修改空指针的过程,可用递归算法。对二叉树按照不同的遍历次序进行线索化,可以得到不同的线索二叉树,包括先序线索二叉树、中序线索二叉树和后序线索二叉树
为了几下遍历过程中访问节点的先后关系,附设一个指针pre始终指向刚刚访问过的节点,而指针p则指向当前访问的节点,由此记录下遍历过程中访问节点的先后关系
以节点p为根的子树中序线索化
算法步骤
1.如果p非空,左子树递归线索化
2.如果p的左孩子为空,则给p加上左线索,将其LTag置为1,让p的左孩子指针指向pre,否则将p的LTag置为0
3.如果pre的右孩子为空,则给pre加上右线索,将其RTag置为1,让pre的有孩子指向p,否则将pre的RTag置为0
4.将pre指向刚刚访问过的节点p,即pre = p
5.右子树递归线索化
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p -> lchild);
if(p -> lchild == NULL)
{
p -> LTag = 1;
p -> lchild = pre;
}
else p -> LTag = 0;
if(pre -> rchild == NULL)
{
pre -> RTag = 1;
pre -> rchild = p;
}
else pre -> RTag = 0;
pre = p;
InThreading(p -> rchild);
}
}
带头结点的二叉树中序线索化
void InOrderThreading(BiThrTree &Thrt,BiThrTree T)
{
Thrt = new BiThrNode;
Thrt -> LTag = 0;
Thrt -> RTag = 1;
Thrt -> rchild = Thrt;
if(!T)Thrt -> lchild = Thrt;
else
{
Thrt -> lchild = T;
pre = T;
InThreading(T);
pre -> rchild = Thrt;
pre -> RTag = 1;
Thrt -> rchild = pre;
}
}
遍历线索二叉树
由于有了节点的前驱和后继信息,线索二叉树的遍历和在执定次序下查找节点的前驱和后继算法都变得简单了,因此,若需要查找节点在所遍历线性序列中的前驱和后继,则采用线索链表所谓存储结构
在先序线索二叉树中查找
1.查找p指针所指节点的前驱:
若p -> LTag为1,则p的左链指示其前驱
若p -> LTag为0,则说明p有左子树。此时p的前驱有两种情况:若*p是双亲的左孩子,则其前驱为双亲节点;否则硬是其双亲左子树上先序遍历最后访问到的节点
2.查找p指针所指节点的后继:
若p -> RTag为1,则p的右链指示其后继
若p -> RTag为0,则说明p有右子树。按先序遍历的规则可知,*p的后继必为左子树根(若存在)或右子树根
在中序线索二叉树中查找
1.查找p指针所指节点的前驱:
若p -> LTag为1,则p的左链指示其前驱
若p -> LTag为0,则说明p有左子树。节点前驱是遍历左子树时访问的最后一个节点(左子树中最右下的节点)
2.查找p指针所指节点的后继:
若p -> RTag为1,则p的右链指示其后继
若p -> RTag为0,则说明p有右子树。按中序遍历的规则可知,节点的后继应是遍历其右子树时访问的第一个节点,即右子树中最左下的节点
在后序线索二叉树中查找
1.查找p指针所指节点的前驱:
若p -> LTag为1,则p的左链指示其前驱
若p -> LTag为0,当p -> RTag也为0时,则p的右链指示其前驱;若p -> LTag为0,而p -> RTag为1时,则p的左链指示其前驱
2.查找p指针所指节点的后继:
若*p是二叉树的根,则其后继为空
若*p是其双亲的有孩子,则其后继为双亲节点
若*p是其双亲的左孩子,且*p没有右兄弟,则其后继为双亲节点
若*p是其双亲的左孩子,且*p有有兄弟,则其后继为双亲的右子树上按后序遍历列出的第一个节点(右子树中最左下的叶节点)
遍历中序线索二叉树
1.指针p指向根节点
2.p为非空或遍历未结束时,循环执行以下操作
沿左孩子向下,到达最左下节点*p,他是中序的第一个节点
访问*p
沿右线索反复查找当前节点*p的后继节点并访问后继节点,直至右线索为0或者遍历结束
转向p的右子树
void InOrderTraverse_Thr(BiThrTree T)
{
p = T -> lchild;
while(p != T)
{
while(p -> LTag == 0)p = p -> lchild;
cout << p -> data;
while(p -> RTag == 1 && p -> rchild != T)
{
p = p -> rchild;
cout << p -> data;
}
p = p -> rchild;
}
}
算法分析
遍历线索二叉树的时间复杂度为O(n),空间复杂度为O(1)
树和森林
树的存储结构
双亲表示法
这种表示方法中,以一组连续的存储单元存储树的节点,每个节点除了数据域data外,还附设一个parent域用以指示其双亲节点的位置
这种存储结构利用了每个节点(除根以外)只有唯一的双亲的性质。这种存储结构下,求取双亲十分方便,求树的根也很容易,但求节点的孩子时需要遍历整个结构
孩子表示法
由于树种每个节点可能有多棵子树,则可用多重链表,即每个节点有多个指针域,其中每个指针指向一棵子树的根节点
若采用第一种节点形式,则多重链表种的节点是同构的,其中d为树的度。由于树中很多节点的度小于d,因此链表中有很多空链域,空间较浪费,不难推出,在一棵有n个度为k的节点的树中必有n(k - 1) + 1个空链域
若采用第二种节点形式,则多重链表中的节点是不同构的,其中d为节点的度,degree域的值同d,此时,虽然能节约存储空间,但操作不方便
另一种方法是,把节点的孩子节点排列起来,看成一个线性表,且以单链表作为存储结构,则n个节点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针由组成一个线性表,为了便于查找,可采用顺序存储结构
孩子兄弟表示法
孩子兄弟表示法又称二叉树表示法,或二叉链表表示法,即以二叉链表作为树的存储结构。链表中节点的两个链域分别指向该节点的第一个孩子节点和下一个兄弟节点,分别命名为firstchild和nextsibling域
树的二叉链表(孩子-兄弟)存储表示
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
利用这种存储结构便于实现各种树的操作。首先易于实现找孩子节点等操作。例如:若要访问节点x的第i个孩子,则只要先从firstchild域找到第一个孩子节点,然后沿着孩子节点的nextsibling域连续走i - 1步,便可找到节点x的第i个孩子。当然,如果为每个节点增设一个parent域,则同样能方便地实现查找双亲的操作
这种存储结构的优点在于它和二叉树的二叉链表表示完全一样,便于将一般的树结构转换为二叉树进行处理,利用二叉树的算法来实现他对树的操作。因此孩子兄弟表示法是应用较为普遍的一种树的存储表示方法
森林与二叉树的转换
从树的二叉链表表示的定义可知,任何一棵和树对应的二叉树,其根节点的右子树必为空。若把森林中第二棵树的根节点看成第一棵树的根节点的兄弟,则同样可导出森林和二叉树的对应关系,这种对应关系说明森林或树与二叉树可以相互转换
森林转换成二叉树
如果F = {T1,T2,...Tm}是森林,则可按如下规则将其转换成一棵二叉树B = {root,LB,RB}
1.若F为空,即m = 0,则B为空树
2.若F非空,即m != 0,则B的根root即森林中第一棵树的根ROOT(T1);B的左子树LB是从T1根节点的子树森林F1 = {T11,T12,...T1m}转换而成的二叉树;其右子树RB是从森林F = {T2,T3,...Tm}转换而成的二叉树
二叉树转换为森林
如果B = (root,LB,RB)是一棵二叉树,则可按如下规则将其转换成森林F = {T1,T2,...Tm}
1.若B为空,则F为空
2.若B非空,则F中第一棵树T1的根ROOT(T1)即为二叉树B的根root;T1中根节点的子树森林F1是由B的左子树LB转换而成的森林;F中除T1之外其余树组成的森林F' = {T2,T3,...Tm}是由B的右子树RB转换而成的森林
树和森林的遍历
树的遍历
由树的结构定义可以引出两种次序遍历树的方法:一种是先根(次序)遍历树,即先访问树的根节点,然后依次先根遍历根的每棵子树;另一种是后根(次序)遍历,即先依次后跟遍历每棵子树,然后访问根节点
按照先根遍历树 RADEBCFGHK
按照后根遍历树 DEABGHKFCR
森林的遍历
先序遍历森林
若森林非空,则可按下述规则遍历
1.访问森林中第一颗树的根节点
2.先序遍历第一棵树的根节点的子树森林
3.先序遍历除去第一颗树之后剩余的树构成的森林
中序遍历森林
若森林非空,则可按下述规则遍历
1.中序遍历森林中第一棵树的根节点和子树森林
2.访问第一颗树的根节点
3.中序遍历除去第一棵树之后剩余的树构成的森林
按照先序遍历森林 得ABCDEFGHIJ
按照中序遍历森林 得BCDAFEHJIG
当森林转化为二叉树时,其第一颗子树森林转化为左子树,剩余的树转换成右子树,则上述森林的先序和中序遍历即为其对应的二叉树的先序和中序遍历
由此可见,当以二叉链表作为树的存储结构时,树的先根遍历和后根遍历可借用二叉树的先序遍历和中序遍历的算法实现
哈夫曼树及其应用
树结构是一种应用非常广泛的结构,在一些特定的应用当中,树具有一些特殊的特点,利用这些特点可以解决很多工程问题
哈夫曼树的基本概念
哈夫曼树又称为最优树,是一类带权路径长度最短的树,在实际中有广泛的用途。哈夫曼树的定义,涉及路径、路径长度、权等概念
1.路径:从树中一个节点到另一个节点之间的分支构成这两个节点之间的路径
2.路径长度:路径上的分支数目称作路径长度
3.树的路径长度:从树根到每一叶子节点的路径长度之和
4.权:赋予某个实体的一个两,是对实体的某个或某些属性的数值化描述。在数据结构中,实体有节点(元素)和边(关系)两大类。所以对应有节点权和边权。节点权或边权具体代表什么意义,由具体情况决定。如果在一棵树中的节点上带有全职,则对应的就有带权数等概念
5.节点的带权路径长度:从该节点到树根之间的路径长度与节点上权值的乘积
6.树的带权路径长度:树种所有叶子节点的带权路径长度之和,通常记作
7.哈夫曼树:假设由m个权值{w1,w2,w3,...,wn},可以构造一棵含n个叶子节点的二叉树,每个叶子节点的权值为,则其中带权路径长度WPI最小的二叉树称作最优二叉树或哈夫曼树
可以发现,在哈夫曼树之中,权值越大的节点离根节点越近,根据这个特点,哈夫曼最早给出了一种构造哈夫曼树的方法,称为哈夫曼算法
哈夫曼树的构造算法
哈夫曼树的构造过程
1.根据给定的n个权值,构造n棵只有根节点的二叉树,这n棵二叉树构成森林F
2.在森林F选取两个节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左右字数上根节点的权值之和
3.在森林F种删除这两棵树,同时将新得到的二叉树加入F中
4.重复2和3,直到F只含一棵树为止
在构造哈夫曼树时,首先选择权值小的,这样能保证权值大的离根较近,这样一来,在计算树的带权路径长度时,自然会得到最小带权路径长度,这种生成算法是一种典型的贪心算法
哈夫曼算法的实现
哈夫曼树是一种二叉树,当然可以采用前面介绍过的通用存储方法,而由于哈夫曼树中没有度为1的节点,则一棵由n个叶子节点的哈夫曼树中共有2n - 1个节点,可以存储在一个大小为2n - 1的一维数组中。树中每个节点还要包含其双亲信息和孩子节点的信息,因此每个节点的存储结构设计如下图所示
typedef struct
{
int weight;
int parent,lchild,rchild;
}HTNode,HuffmanTree;
哈夫曼树的各节点存储在由HuffmanTree定义的动态分配数组中,为了实现方便,数组的0号单元不能用,从1号单元开始使用,所以数组的大小为2n。将叶子节点集中存储在前面部分的n个位置,而后面的n - 1个位置存储其余非子叶节点
构造哈夫曼树
1.初始化:首先动态申请2n个单元;然后循环2n - 1次,从1号单元开始,依次将1 - 2n - 1所有单元中的双亲,左孩子,右孩子都初始化为0;最后循环n次,输出前n个节点的权值
2.创建树:循环n - 1次,通过n - 1次的选择、删除与合并来创建哈夫曼树。选择是从当前森林中选择双亲为0且权值最小的两个树节点s1、s2;删除是指将节点s1、s2的双亲改为非0;合并就是将s1、s2的权值和作为一个新节点的权值依次存入数组的n + 1号及之后的单元之中。同时记录这个新系欸但左孩子的下标s1,右孩子的下标s2
void CreatHuffmanTree(HuffmanTree &HT,int n)
{
if(n <= 1)return;
int m = 2 * n - 1;
HT = new HTNode[m + 1];
for(int i = 1;i <= m;i ++ )
{
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
for(int i = 1;i <= n;i ++ )cin >> HT[i].weight;
for(int i = n + 1;i <= m;i ++ )
{
Select(HT,i - 1,s1,s2);
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
哈夫曼编码
哈夫曼编码的主要思想
在进行数据压缩时,为了使压缩后的数据文件尽可能短,可采用不定长编码,其基本思想是:为出现次数较多的字符编译较短的编码,为确保对数据文件进行有效的压缩和对压缩文件进行正确的解码,可以利用哈夫曼树来设计二进制编码
1.前缀编码:如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码。前缀编码可以保证对压缩文件进行解码时不产生二义性,确保正确解码
2.哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对其书中的每个左分支赋予0,对每个右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码
哈夫曼编码具有下面两个性质
性质1:哈夫曼编码时前缀编码
性质2:哈夫曼编码都是最优前缀编码
哈夫曼编码的算法实现
在构造哈夫曼树之后,求哈夫曼编码的主要思想是,依次以叶子为出发点,向上回溯至根节点为止,回溯时走左分支则生成代码0,走右分支则生成代码1
由于