【数据结构】-树
树
树的定义
树是n( n ≥ 0 n\geq 0 n≥0)个结点的有限集。当n=0时,称为空树。在任何一个非空树中应满足:
- 有且仅有一个特定的称为根的结点。
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,T3,…,Tm,其中每个集合又是一棵树,并且称为根的子树。
树是一种递归的数据结构。
具有以下两个特点:
- 树的根节点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中的所有结点可以有零个或多个后继。
树适合于表示具有层次结构的数据。树中的某个结点(除根结点外)最多只和上一层的一个结点(即其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的数中有n-1
条边。
基本概念
- 考虑结点K。根A到结点K的唯一路径上任一结点,称为结点K的祖先。结点K是结点B的子孙。结点E称为K的双亲,K为结点E的孩子。K和L为兄弟。
- 结点的度:一个结点的孩子个数。树的度:树中结点的最大的度数。B的度为2,D的度为3,树的度为3。
- 分支结点(非终端结点):度大于0的结点。叶子结点(终端结点):度为0的结点。
- 结点的层次从树根开始定义,根结点为第1层,它的子结点为第2层。双亲在同一层的结点互为堂兄弟。
结点的深度:从根结点开始自顶向逐层累加的。
结点的高度:从叶子结点开始自底向上逐层累加的。
树的高度(深度):树中结点的最大层数。 - 有序树:树中结点的各子树从左到右是有次序的,不能互换。相反为无序树。
- 树中两个结点的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
- 森林:m( m ≥ 0 m\geq 0 m≥0)棵互不相交的树的集合。
树的性质
基本性质:
- 树中的结点数=所有结点的度数之和 + 1。
- 度为m的树中第i层上至多有mi-1个结点( i ≥ 1 i\geq 1 i≥1)。
- 高度为h的m叉树至多有(mh-1)/(m-1)个结点。
- 具有n个结点的m叉树的最小高度为 ⌈ \lceil ⌈logm(n(m-1)+1) ⌉ \rceil ⌉。
例题:
- 含有n个结点的三叉树的最小高度是多少?
1.解答:
要求含有n个结点的三叉树的最小高度,那么满足条件的一定是一颗完全三叉树(要保证每个结点的度都是最大的,才可以保证树高最低),设含有n个结点的完全三叉树的高度为h,第h层至少有一个结点,至多3h-1个结点(性质二)。则有:
1+32-1+33-1+···+3h-2<n <=1+32-1+33-1+···+3h-2+3h-1
即(3h-1-1)/2<n<=(3h-1)/2,得3h-1<2n+1<=3h,也即h<log3(2n+1)+1,h>=log3(2n+1)。
由于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的结点一一对应时,称为完全二叉树。
特点:
- 若i≤ ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋,则结点i为分支结点,否则为叶子结点。
- 叶子结点只可能在层次最大的两层出现,对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。
- 若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。
- 按层序编号后,一旦出现某结点为叶子结点或只有左孩子,则编号大于i的结点均为叶子及诶点。
- 若n为奇数,则每个分支结点都有左孩子和右孩子;若n为偶数,则编号最大的分支结点(编号为n/2)只有左孩子,没有右孩子,其余分支结点左右孩子都有。
二叉排序树
左子树上的所有关键字均小于根结点的关键字;右子树上的所有关键字均小于根结点的关键字;左子树和右子树又各是一棵二叉排序树。
平衡二叉树
树上任一结点的左子树与右子树高度差不超过1。
二叉树的性质
- 非空二叉树上的叶子结点数等于度为2 的结点数加1,即n0=n2+1。
证明:
设度为0,1,2的结点个数分别为n0,n1,n2,总结点数为 n=n0+n1+n2。
再看二叉树的分支数,除根节点外,其余结点都有一个分支进入,设B为分支总数,则n=B+1。
由于这些分支是由度为1或2的结点射出,所以又有B=n1+2n2。于是得n0+n1+n2 = n1+2n2+1,则n0=n2+1。 - 非空二叉树上第k层上至多有2k-1个结点(k≥1)。
第1层至多有21-1=1个结点,第2层至多22-1=2个结点。 - 高度为h的二叉树至多有2h-1个结点(h≥1)。
该结论利用性质2求前h项的和,即等比数列求和的结果。 - 对完全二叉树从上到下、从左到右的顺序依次编号1,2,…,n,则有以下关系:
① 当i>1时,结点i的双亲的编号为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋,即当i为偶数时,其双亲编号为i/2,它是双亲的左孩子;当i为奇数时,其双亲的编号为(i-1)/2,它是双亲的右孩子。
② 当2i≤n时,结点i的左孩子编号为2i,否则我左孩子。
③ 当2i+1≤n时,结点i的右孩子编号为2i+1,否则无右孩子。
④ 结点i所在层次(深度)为 ⌊ l o g 2 i ⌋ \lfloor log_2i \rfloor ⌊log2i⌋+1。 - 具有n个(n>0)结点的完全二叉树的高度为
⌈
\lceil
⌈log2(n+1)
⌉
\rceil
⌉或
⌊
l
o
g
2
n
⌋
\lfloor log_2n \rfloor
⌊log2n⌋+1。
设高度为h,根据性质3和完全二叉树的定义有2h-1-1<n≤2h-1或2h-1-1≤n<2h得2h-1<n+1≤2h,即h-1<log2(n+1)≤h,因为h为正整数,所以h= ⌈ \lceil ⌈log2(n+1) ⌉ \rceil ⌉。或得h-1≤log2n<h,所以h= ⌊ l o g 2 n ⌋ \lfloor log_2n \rfloor ⌊log2n⌋+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;
}
}
- 求中序线索二叉树中结点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种情况来处理:
- 若被删除结点z的叶结点,则直接删除,不会破坏二叉排序树的性质。
- 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。
- 若结点z有左、右两棵子树,则令z的直接后驱(或直接前驱)替代z,然后从二叉排序树中删去这个直接后驱(或直接前驱),这样就转换成了第一或第二种情况。
二叉排序树的查找效率分析
二叉排序树的查找效率,主要取决于树的高度。若二叉排序树的左、右子树的高度之差的绝对值不超过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)最小的二叉树称为哈夫曼树,也称最优二叉树