树是n( n ≥ 0 n\geq 0 n≥0)个结点的有限集。当n=0时,称为空树。在任何一个非空树中应满足:
树是一种递归的数据结构。
具有以下两个特点:
树适合于表示具有层次结构的数据。树中的某个结点(除根结点外)最多只和上一层的一个结点(即其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的数中有n-1
条边。
基本性质:
例题:
- 含有n个结点的三叉树的最小高度是多少?
1.解答:
要求含有n个结点的三叉树的最小高度,那么满足条件的一定是一颗完全三叉树(要保证每个结点的度都是最大的,才可以保证树高最低),设含有n个结点的完全三叉树的高度为h,第h层至少有一个结点,至多3h-1个结点(性质二)。则有:
1+32-1+33-1+···+3h-2
即(3h-1-1)/2
由于h只能为正整数,h= ⌈ \lceil ⌈log3(2n+1) ⌉ \rceil ⌉,最小高度为 ⌈ \lceil ⌈log3(2n+1) ⌉ \rceil ⌉。
- 已知一颗度为4的树中,度为0,1,2,3的结点数分别为14,4,3,2,求该树的结点总数n和度为4的结点个数,并给出推导过程。
2.解答:
设树中度为i(i=0,1,2,3,4)的结点数为ni,那么结点总数n=n0+n1+n2+n3+n4,即n=23+n4,根据“总结点数=所有结点度的和 + 1”的结论,有n=0+n1+2n2+3n3+4n4+1,即有n=17+4n4。
综合两式得n4=2,n=5。结点总数为25,度为4的结点个数为2。
常用于求解树结点于度之间关系的有:
①总结点数 = n0+n1+n2+···+nm。
②总分支数 = 1n1+2n2+···+mnm(度为m的结点引出m条分支)
③总结点数 = 总分支数 + 1。
二叉树每个结点至多只有两棵子树(二叉树不存在度大于2的结点),子树有左右之分,其次序不能任意颠倒。
二叉树是n(n>=0)个结点的有限集合:
① 空二叉树,n=0。
② 由一个根节点和两个互不相交的被称为根的左子树和右子树组成。
二叉树和度为2的有序树的区别:
① 度为2的树至少有三个结点,而二叉树可以为空。
② 度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子无须区分其左右次序,而二叉树无论其孩子数是否为2,均需确定其左右次序,二叉树的结点次序是确定的。
满二叉树
高度为h,且含有2h-1个结点的二叉树称为满二叉树,即每层都含有最多的结点。满二叉树的叶子结点都集中在二叉树的最下层,并且除叶子结点之外的每个结点度均为2。
满二叉树按层序编号:对于编号i的结点,若有双亲,则其双亲为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋,若有左孩子,则左孩子为2i;若有右孩子,则右孩子为2i+1。
完全二叉树
高度为h,有n个结点的二叉树,当且仅当其每个结点都与高度h的满二叉树中
编号为1~n的结点一一对应时,称为完全二叉树。
特点:
二叉排序树
左子树上的所有关键字均小于根结点的关键字;右子树上的所有关键字均小于根结点的关键字;左子树和右子树又各是一棵二叉排序树。
平衡二叉树
树上任一结点的左子树与右子树高度差不超过1。
用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下标为i-1的分量中。
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一反应结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树的位置,以及结点之间的关系。
最坏的情况下,一个高度为h且只有h个结点的单支树却需要占据近2h-1个存储单元。
顺序存储的空间利用率低,因此采用链式存储结构,用链表结点存储二叉树的每个结点。
二叉链表至少包含3个域:数据域data
、左指针域lchild
和右指针域rchild
。
二叉树的链式存储结构描述如下:
/**
* 二叉树的链式存储结构
* @return
*/
typedef struct BiTNode {
ElemType data;//数据域
struct BiTNode *lchild, *rchild;//左、右孩子指针
} BiTNode, *BiTree;
在含有n个结点的二叉链表中,含有n+1个空链域。
二叉树的遍历是指按某条搜索路径访问树的每个结点,使得每个结点均被访问一次,而且仅被访问一次。
遍历一棵二叉树便要决定对根节点N、左子树L和右子树R的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有:先序(NLR)、中序(LNR)和后序(LRN)三种遍历算法。
先序遍历
先序遍历(PreOrder)
若二叉树为空,则什么也不做;否则,
/**
* 先序遍历
* @return
*/
void PreOrder(BiTree T) {
if (T != NULL) {
visit(T); //访问根节点
PreOrder(T->lchild);//递归遍历左子树
PreOrder(T->rchild);//递归遍历右子树
}
}
中序遍历
中序遍历(InOrder)
若二叉树为空,则什么也不做;否则,
/**
* 中序遍历
* @return
*/
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild);//递归遍历左子树
visit(T);//访问根节点
InOrder(T->rchild);//递归遍历右子树
}
}
后序遍历
后序遍历(PostOrder)
若二叉树为空,则什么也不做;否则,
递归算法如下:
/**
* 后序遍历
* @return
*/
void PostOrder(BiTree T) {
if (T != NULL) {
PostOrder(T->lchild);//递归遍历左子树
PostOrder(T->rchild);//递归遍历右子树
visit(T);//访问根节点
}
}
递归算法和非递归算法的转换
中序遍历的非递归算法
/**
* 中序遍历的非递归算法
* @return
*/
void InOrder2(BiTree T) {
SqStack *S;
InitStack(S);//初始化栈S
BiTree p = T;//p是遍历指针
while (p || !IsEmpty(S)) {//栈不空或p不空时循环
if (p) {//一路向左
Push(S, p);//当前结点入栈
p = p->lchild;//左孩子不空,一直向左走
} else {//出栈,并转向出栈结点的右子树
Pop(S, p);//栈顶元素出栈,访问出栈结点
visit(p);
p = p->rchild;//向右子树走,p赋值为当前结点的右孩子
}//返回while循环继续进入if-else语句
}
}
先序遍历的非递归算法:
/**
* 先序遍历的非递归算法
* @return
*/
void PreOrder2(BiTree T) {
SqStack *S;//初始化栈S
InitStack(S);//p是遍历指针
BiTree p = T;
while (p || !IsEmpty(S)) {//栈不空或p不空时循环
if (p) {//一路向左
visit(p);//访问当前结点
Push(S, p);//当前结点入栈
p = p->lchild;//左孩子不空,一直向左走
} else {//出栈,并转向出栈结点的右子树
Pop(S, p);//栈顶元素出栈
p = p->rchild;//向右子树走,p赋值为当前结点的右孩子
}//返回while循环继续进入if-else语句
}
}
层次遍历
层次遍历需要借助一个队列。先将二叉树根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根结点入队;若它有右子树,则将右子树根结点入队。然后出队,访问出队结点······如此反复,直至队列为空。
二叉树的层次遍历算法如下:
/**
* 层次遍历算法
* @return
*/
void LevelOrder(BiTree T) {
SqQueue Q;
InitQueue(&Q);//初始化辅助队列
BiTree p;
EnQueue(&Q, T);//将根结点入队
while (!isEmpty(Q)) {//队列不空则循环
DeQueue(&Q, T);//队头结点出队
visit(p);//访问出队结点
if (p->lchild != NULL) {
EnQueue(&Q, p->lchild);//左子树不空,则左子树g根结点入队
}
if (p->rchild != NULL) {
EnQueue(&Q, p->rchild);//右子树不空,则右子树根结点入队
}
}
}
由遍历序列构造二叉树
由二叉树的先序序列和中序序列可以唯一确定一棵二叉树。
由二叉树的后序序列和中序序列可以唯一确定一棵二叉树。
由二叉树的层序序列和中序序列可以唯一确定一棵二叉树。
线索二叉树的基本概念
遍历二叉树是以一定规则将二叉树中的结点排列成一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(第一个和最后一个结点除外)都有一个直接前驱和直接后驱。
规定:若无左子树,领lchild
指向其前驱结点;若无右子树,领rchild
指向其后继结点。
线索二叉树的存储结构
/**
* 线索二叉树的存储结构
*/
typedef struct ThreadNode {
ElemType data;//数据元素
struct ThreadNode *lchild, *rchild;//左、右孩子指针
int ltag, rtag;//左、右线索标志
} ThreadNode, *ThreadTree;
以这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点前驱和后继的指针称为线索,加上线索的二叉树称为线索二叉树。
中序线索二叉树的构造
二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱和后继的信息只有在遍历时才能得到,因此线索化的实质就是遍历一次二叉树。
以中序线索二叉树的建立为例。附设指针pre
指向刚刚访问过的结点,指针p
指向正在访问的结点,即pre
指向p
的前驱。在中序遍历过程中,检查p
的坐指针是否为空,若为空就将它指向pre
;检查pre
右指针是否为空,若为空就将它指向p
。
中序遍历对二叉树线索化的递归算法:
/**
* 中序遍历对二叉树线索化的递归算法
* @param p
* @param pre
*/
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);//递归,线索化右子树
}
}
通过中序遍历建立中序线索二叉树的主过程算法如下:
/**
* 中序遍历建立中序线索二叉树的主过程
* @param T
*/
void CreateInThread(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {//非空二叉树,线索化
InThread(T, pre);//线索化二叉树
pre->rchild = NULL;//处理遍历的最后一个结点
pre->rtag = 1;
}
}
中序线索二叉树的遍历
中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。在对其进行遍历时,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空。在中序线索二叉树中找结点后继的规律是:若其右标志位“1”,则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。不含头结点的线索二叉树的遍历算法如下:
ThreadNode *Firstnode(ThreadNode *p) {
while (p->ltag == 0) {
p = p->lchild;//最左下结点(不一定是叶结点)
return p;
}
}
ThreadNode *Nextnode(ThreadNode *p) {
if (p->rtag == 0) {
return Firstnode(p->rchild);
} else {
return p->rchild;//rtag==1,直接返回后继线索
}
}
void Inorder(ThreadNode *T) {
for (ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p)) {
visit(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个孩子链表。
孩子兄弟表示法又称二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟表示法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针。
孩子兄弟表示法的存储结构描述:
typedef struct CSNode {
ElemType data;//数据域
struct CSNode *firstchild, *nextsibling;//第一个孩子和右兄弟指针
} CSNode, *CSTree;
二叉排序树的定义
二叉排序树也称二叉查找树,具有以下特性:
左子树结点值<根结点值<右子树结点值
对二叉排序树进行中序遍历,可以得到一个递增有序序列。
二叉排序树的查找
从根结点开始,沿某个分支逐层向下比较的过程。若二叉排序树非空,先将给定值与根结点的关键字比较,若相等,则查找成功;若不等,如果小于根结点的关键字,则在根结点的左子树上查找,否则在根结点的右子树上查找。
二叉排序树的非递归查找算法:
/**
* 二叉排序树的非递归查找算法
* @return
*/
BSTNode BST_Search(BiTree T, ElemType key) {
while (T != NULL && key != T->data) {//若树空或等于根结点的值,则结束循环
if (key < T->data) {//小于,则在左子树上查找
T = T->lchild;
} else {//大于,则在右子树上查找
T = T->rchild;
}
}
return T;
}
二叉树排序树的插入
插入结点的过程如下:若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点的值,则插入到右子树。插入的结点一定是一个新添加的叶结点,且是查找失败时的查找路径上访问的最后一个结点的左孩子或右孩子。
二叉排序树插入操作的算法如下:
/**
* 二叉排序树的插入操作
* @return
*/
int BST_Insert(BiTree *T, KeyType k) {
if (T == NULL) { //原树为空,新插入的结点为根结点
*T = (BiTree) malloc(sizeof(BiTNode));
(*T)->data = k;
(*T)->lchild = (*T)->rchild = NULL;
return 1;//返回1,插入成功
} else if (k == (*T)->data) {//树中存在相同关键字的结点,插入失败
return 0;
} else if (k < (*T)->data) {//插入到T的左子树
return BST_Insert((*T)->lchild, k);
} else {//插入到T的右子树
return BST_Insert((*T)->rchild, k);
}
}
二叉排序树的构造
构造二叉排序树的算法如下:
/**
* 二叉排序树的构造
* @return
*/
void Create_BST(BiTree *T, KeyType str[], int n) {
T = NULL;//初始时T为空树
int i = 0;
while (i < n) {//依次将每个关键字插入到二叉排序树中
BST_Insert(T, str[i]);
i++;
}
}
二叉树排序树的删除
在二叉排序树中删除一个结点时,不能把以该结点为根的子树上的结点都删除,必须先把被删除结点从存储二叉排序树的链表上摘下,将因删除结点而断开的二叉链表重新链接起来,同时确保二叉排序树的性质不会丢失。删除操作的实现过程按3种情况来处理:
二叉排序树的查找效率分析
二叉排序树的查找效率,主要取决于树的高度。若二叉排序树的左、右子树的高度之差的绝对值不超过1,则这样的二叉排序树称为平衡二叉树,它的平均查找长度为O(log2n)。若二叉排序树是一个只有右(左)孩子的单支树,则其平均查找长度为O(n)。
在最坏的情况下,即构造二叉排序树的输入序列是有序的,则会形成一个倾斜的单支树,此时二叉排序树的性能显著变坏,树的高度也增加为元素个数n。
(a)查找成功的平均查找长度为
ASLa=(1+2*2+3*4+4*3)/10=2.9
(b)查找成功的平均查找长度为
ASLb=(1+2+3+4+5+6+7+8+9+10)/10=5.5
平衡二叉树的定义
保证任意结点的左、右子树的高度差的绝对值不超过1,这样的二叉树称为平衡二叉树。
平衡因子:定义结点左子树与右子树的高度差为该结点的平衡因子。平衡二叉树结点的平衡因子的值只可能是-1、0或1。
哈夫曼树的定义
树中结点被赋予一个表示某种意义的数值,称为该结点的权。从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度。记为:
WPL= ∑ i = 1 n w i l i \displaystyle \sum^{n}_{i=1}{w_il_i} i=1∑nwili
wi是第i个叶结点所带的权值,li是该叶结点到根结点的路径长度。
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树