树是 n ( n ≥ 0 ) n(n \ge 0) n(n≥0) 个结点的有限集合, n = 0 n = 0 n=0 时称为空树。
任意一个非空树都应该满足:
树的定义是具有递归特性的,是一种递归的数据结构。在作为一种逻辑结构的同时,树也是一种分层结构,并具有两个特点:
二叉树的一种特殊的树形结构,其特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能交换颠倒(即二叉树是有序树)。
与一般的树相似,二叉树也有递归的形式定义,二叉树是 n ( n ≥ 0 ) n(n\ge0) n(n≥0) 个结点的有限集合,为空树时 n = 0 n=0 n=0 。由一个根结点和连个互不相交的根的左子树和右子树组成,其中左、右子树均是二叉树。
二叉树的五种基本形态:
二叉树与度为 2 的树的区别:
满二叉树
一棵高度为 h h h ,且含有 2 h − 1 2^{h}-1 2h−1 个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点的度数都为2。
对满二叉树按层序编号,约定从根结点开始编号,根结点编号为 1,自上而下,从左往右,每个结点对应一个编号。对于编号为 i i i 的结点,若有双亲结点,在则其双亲编号为 ⌊ i 2 ⌋ \lfloor \frac{i}{2}\rfloor ⌊2i⌋;若有左孩子,则左孩子为 2 i 2i 2i ;若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1。
完全二叉树
设高度为 h h h,有 n n n 个结点的二叉树,当且仅当其每个结点都与高度为 h h h 的满二叉树中编号
为 1 ∼ n 1\sim n 1∼n 的结点一一对应时,称为完全二叉树。
二叉排序树
一棵二叉树或者空二叉树,或是具有以下性质的二叉树:左子树上所有的结点的关键字均小于根结点的关键字,右子树的上所有结点的关键字均大于根结点的关键字,左右子树又各是二叉排序树。
平衡二叉树
树上任一结点的左子树和右子树的深度之差不超过 1。
二叉树的顺序存储结构是指用一组地址连续的存储单元依次自上而下、从左到右存储完全二叉树上的结点,即将完全二叉树上编号为 i i i 的结点存储在数组下标为 i − 1 i-1 i−1 的数组分量中,然后通过一定方式确定结点在逻辑上的父子或兄弟关系。
链式结构是指用一个链表来存储二叉树,二叉树中的每个结点用链表中的一个链结点来存储。在二叉树中,结点结构通常包括若干数据域和若干指针域,则二叉链表至少包含三个域:数据域 d a t a data data、左指针域 l c h i l d lchild lchild、右指针域 r c h i l d rchild rchild。
二叉树的链式存储结构描述:
typedef struct BiTNode{
ElemType data; //定义数据域
strucr BiTNode *lchild, *rchild; //定义左、右指针
}BiTNode, *BiTree;
遍历,是指按照某条搜索路径访问树中的每一个结点,使得每个结点均会被访问一次,而且仅被访问一次。
由二叉树的递归定义可知,遍历一棵二叉树首先需要确定对根结点、左子树和右子树的访问顺序。常见的遍历次序有先序遍历 (NLR) 、中序遍历 (LNR) 和后序遍历 (LRN) 三种。
先序遍历
若二叉树为空,则不采取操作,否则:
对应的递归算法:
void PreOrder(BiTree T){
if(T != NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchid); //递归遍历右子树
}
}
中序遍历
若二叉树为空,则不进行操作,否则:
对应的递归算法:
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild); //递归访问左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归访问右子树
}
}
后序遍历
若二叉树为空,则不进行操作,否则:
对应的递归算法:
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
递归算法和非递归算法的转换
借助栈,可将二叉树的三种递归遍历算法转换为非递归算法。
以中序遍历为例:先扫描(非访问操作)根结点的所有左结点并将其一一入栈,然后出栈一个结点 ∗ p *p ∗p(该结点没有左孩子结点或左孩子结点已经全被访问过)并访问。然后扫描该结点的右孩子结点并将其入栈,再扫描该右孩子结点的所有左结点并入栈。重复上述操作直到栈空为止。
中序遍历的非递归算法:
void InOrder_NonRrecurrence(BiTree T){
InitStack(s); //创建栈并初始化
BiTNode p = T; //创建遍历指针 p
while(p || !IsEmpty(s)){ //若指针 p 不空或者栈不空时循环
if(p){ //若指针 p 不空(非空左子树),则遍历左子树入栈
Push(s, p); //p 指向的结点入栈
p = p->lchild; //指针 p 指向左子树
}
else{ //根结点的所有左结点均入栈
Pop(s, p); //一个结点出栈
visit(p); //访问该结点
p = p->rchild;
//指针 p 指向该结点的右子树,下一个循环遍历该右子树的所有左结点
}
}
}
层次遍历
如图所示为二叉树的层次遍历,即按照箭头所指方向,按照层次1,2,3,4的顺序,横向遍历对二叉树中的结点进行访问。
进行层次遍历需要借助队列:先将二叉树根结点入队,然后出队并访问该结点,若有左子树,则将左子树根结点入队;若有右子树,则将右子树根结点入队。然后出队并对出队结点进行访问,重复上述操作直到队列为空。
二叉树的层次遍历算法:
void LevelOrder(BiTree T){
InitQueue(Q); //创建空队列 Q
BiTree p; //创建遍历指针 p
EnQueue(Q, T); //根结点入队
while(! IsEmpty Q){ //队列非空则继续循环进行遍历
DeQueue(Q, p); //队头元素出队
visit(p); //访问出队结点
if(p->lchild != NULL) //若左子树存在
EnQueue(Q, p->Lchild); //左子树根结点入队
if(p->child != NULL) //若右子树存在
EnQueue(Q, p->rchild); //右子树根结点入队
}
}
由遍历序列构造二叉树
基本概念
在有 n n n 个结点的二叉树中,有 n + 1 n+1 n+1 个空指针(每个叶子结点有 2 个空指针,度为 1 的结点有 1 个空指针,空指针总 n 1 + 2 ∗ n 0 n_{1} + 2*n_{0} n1+2∗n0,又有 n 0 = n 2 + 1 n_{0} = n_{2} +1 n0=n2+1,所以空指针总数为 n 0 + n 1 + n 2 + 1 = n + 1 n_{0} +n_{1}+n_{2}+1 = n +1 n0+n1+n2+1=n+1)。
在二叉树线索化时,通常规定:若无左子树,则令 l c h i l d lchild lchild 指向其前驱结点;若无右子树,则令 r c h l i d rchlid rchlid 指向其后继结点。增加两个标志域表明当前指针域所指向对象是前、后驱结点还是左、右子树结点。
其中,标志域的含义为:
l t a g { 0 l c h i l d 域 指 向 结 点 的 左 孩 子 1 l c h i l d 域 指 向 结 点 的 前 驱 ltag\left\{ \begin{array}{rcl} 0 & lchild域指向结点的左孩子\\ 1 & lchild域指向结点的前驱\\ \end{array} \right. ltag{01lchild域指向结点的左孩子lchild域指向结点的前驱
r t a g { 0 r c h i l d 域 指 向 结 点 的 右 孩 子 1 r c h i l d 域 指 向 结 点 的 后 继 rtag\left\{ \begin{array}{rcl} 0 & rchild域指向结点的右孩子\\ 1 & rchild域指向结点的后继\\ \end{array} \right. rtag{01rchild域指向结点的右孩子rchild域指向结点的后继
线索二叉树的存储结构描述:
typedef struct ThreadNode{
ElemTyped data; //数据域
struct ThreadNode *lchild, *rchild; //左、右指针域
int ltag, rtag; //左、右线索标志
}ThreadNode, *ThreadTree; //类型名申明
以上述结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点前驱和后继的指针称为线索。具有线索的二叉树称为线索二叉树。对二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化。
线索二叉树的构造
对二叉树的线索化,实质上就是遍历一次二叉树,在遍历的过程中,检查当前结点的左、右指针域是否为空,若为空,将其改为指向前驱结点或后继结点的线索。
通过中序遍历对二叉树线索化的递归算法:
void InThread(ThreadTree &p, ThreadTree &pre) {
//指针 p 指向二叉树根结点,指针 pre 指向中序遍历时上一个刚访问过的结点
if(p != NULL){ //若二叉树不为空
InThread(p->lchild, pre); //递归线索化左子树
if(p->lchild == NULL){ //若左子树为空,建立前驱线索
p->lchild = pre; //lchild指针指向上一个刚访问过的结点
p->ltag = 1; //线索标志表明此时lchild指向前驱结点
}
if(pre != NULL && pre->rchild == NULL){
//不存在上一个访问的结点 或 上一个访问的结点的右子树不存在
pre->rchild = p; //rchild指向父亲结点
pre->rtag = 1; //线索标志表明此时rchild指向后继结点
}
pre = p; //pre 指针指向父亲结点
InThread(p->rchild, pre); //递归线索化右子树
}
}
通过中序遍历建立线索二叉树的主过程算法:
void CreatInThread(ThreadTree T){
ThreadTree pre = NULL; //创建线索指针 pre
if(T != NULL){
InThread(T, pre); //线索化二叉树
pre->rchild = NULL; //处理遍历到的最后一个结点
pre->rtag = 1; //线索标志表明此时rchild 指向后继结点
}
}
为了操作方便,在二叉树的线索链表上添加头结点,并让头结点的 l c h i l d lchild lchild 域的指针指向二叉树的根结点,让 r c h i l d rchild rchild 域的指针指向中序遍历时访问到的最后一个结点。反之,让中序序列的第一个结点和最后一个结点的 l c h i l d lchild lchild 域的指针都指向头结点。
线索二叉树的遍历
中序线索化的二叉树主要为访问运算服务,因为结点中包含了线索二叉树的前驱和后继信息,所以遍历不需要借助栈。利用线索二叉树,可以实现二叉树遍历的非递归算法。
ThreadNode *FirstNode(ThreadNode *p){
while(p->ltag == 0) //若父亲结点的child指针指向左孩子,则继续遍历
p = p->lchild; //直到遍历到最左下的结点
return p;
}
ThreadNode *NextNode(ThreadNode *p){
if(p->rtag == 0) //若结点 p 的 rchild 指向右孩子
return FirstNode(p->rchild); //递归遍历右子树
else //rtag == 1
return p->rchild; //直接返回后继线索
}
void InOrder_NonRecurrence(ThreadTree T){
for(ThreadNode *p = FirstNode(T); p != NULL; p = NextNode(p))
//初始条件:中序序列第一个结点
//循环条件:父亲结点不为空
//步长条件:父亲结点为中序序列下一个结点
visit(p); //访问结点 p
}
双亲表示法
采用一组连续空间存储每个结点,同时在每个结点中增设一个伪指针用来指示其双亲结点在数组中的位置。其中,根结点下标为 0,其伪指针域为 -1。
双亲表示法的存储结构描述:
#define MAX_TREE_SIZE 100 //树中最多结点数定义
typedef struct{
ElemType data; //数据域
int parent; //双亲位置域(数组下标)
}PTNode; //树结点类型定义
typedef struct{
PTNode nodes[MAX_TREE_SIZE]; //双亲表示数组
int n; //结点数计数
}PTree; //树类型定义
双亲表示法的存储结构利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快得到每个结点的双亲结点,但搜索孩子结点时需要遍历整个树。
孩子表示法
将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时 n n n 个结点就有 n n n 个孩子链表(叶子结点的孩子链表为空表)。
该存储方式寻找孩子结点非常快捷,但需要双亲结点则需要遍历 n n n 个结点中孩子链表指针域所指向的 n n n 个孩子链表。
孩子兄弟表示法
孩子兄弟表示法又称为二叉树表示法,是以二叉链表作为树的存储结构,每个结点包括三部分:结点值、指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)。
孩子兄弟表示法的存储结构描述:
typedef struct CSNode{
ElemType data; //数据域
struct CSNode *firstchild, *nextsibling;
//指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针
}CSNode, *CSTree;
该存储方式最大的优点是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子结点。但从当前结点查找其双亲结点较为复杂,时间开销较大。
由于二叉树和树都可以用二叉链表作为存储结构,因此以二叉链表作为媒介可以导出树与二叉树的对应关系,即给定一棵树,可以找到唯一的一棵二叉树与之对应。
树的遍历操作:是以某种方式访问树中的每个结点且仅访问一次。
森林的遍历操作:
二叉排序树 (BST),又称为二叉查找树。二叉排序树或是空树,或是具有下列特性的非空二叉树:
由上可知, 二叉排序树是一个递归的数据结构;
左子树结点值 ≤ \le ≤ 根节点值 ≤ \le ≤ 右子树结点值。
二叉排序树的查找从根结点开始,沿某个分支逐层向下进行比较的过程。若非空,则将给定值与根结点值进行比较,若相等则查找成功;若不等,当给定值比根结点值小则在根结点的左子树中进行查找,否则在根结点的右子树中进行查找。
二叉排序树的非递归查找算法:
BSTNode *BST_Search(BiTree T, ElemType key, BSTNode *&p){
p = NULL; //p 指针指向被查找结点的双亲结点,用于插入和删除
while(T != NULL && key != T->data){ //树为空或查找成功,退出循环
p = T;
if(key < T->data) //给定值比根结点值小
T = T->lchild; //则在根结点的左子树中进行查找
else
T = T->rchild; //否则在根结点的右子树中进行查找
}
return T;
}
若原二叉排序树为空,则直接插入结点;若关键字值小于根结点值,则插入左子树,否则插入右子树。
int BST_Insert(BiTree T, keyType k){
if(T == NULL){ //树为空
T = (BiTree)malloc(sizeof(BSTNode))); //创建新结点
T->key = k;
T->lchild = NULL;
T->rchild = NULL;
return 1;
}
else if(k == T->key) //树中已存在关键字值
return 0;
else if(k < T->key) //关键字值小于根结点值
return BST_Insert(T->lchild, k); //插入 T 的左子树
else //关键字值大于根结点值
return BST_Insert(T->rchild, k); //插入 T 的右子树
}
每读入一个元素,就建立一个新结点,若二叉排序树非空,则将新结点的值与根结点的值进行比较,若小于根结点的值则插入左子树,否则插入插入右子树;若二叉排序树为空,则将新结点作为二叉排序树的根结点。
构造算法:
void Creat_BST(BiTree &T, KeyType str[], int n){
T = NULL; //初始时 T 为空树
int i = 0;
while(i < n){ //依次创建给定数量的结点
BST_Insert(T, str[i]); //用关键字数组 str[] 插入创建二叉排序树
i++;
}
}
删除操作的实现需要按照三种情况来处理:
在插入和删除二叉树结点时,保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(Balanced Binary Tree),简称平衡树(AVL)。定义结点左、右子树的高度差为该结点的平衡因子,平衡二叉树结点的平衡因子的值只可能为 -1,0,1。
每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为该结点的插入而导致不平衡,若不平衡则先找到插入路径上最小不平衡子树(离插入结点最近的、平衡因子绝对值大于 1 的结点,将以该结点为根的子树),保证二叉排序树特性的前提下,调整各结点的位置关系以重新达到新的平衡。
调整规律
平衡二叉树的查找与二叉排序树的查找过程相同。
在查找过程中,与给定值进行比较的关键字个数不超过树的深度。含有 n n n 个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O(log_{2}{n}) O(log2n),因此平衡二叉树的平均查找长度为 O ( l o g 2 n ) O(log_{2}{n}) O(log2n)。
在含有 n n n 个带权叶子结点的二叉树中,带权路径长度 (WPL) 最小的二叉树称为哈夫曼树,也称为最优二叉树。
构造算法描述:
由构造过程可得,哈夫曼树具有以下特点:
0 和 1 表示左子树还是右子树没有明确规定。因此,左、右结点的顺序是任意的, 所以构造出的哈夫曼树并不唯一, 但是各哈夫曼树的带权路径长度相同且为最优。