树的定义是递归的,是一种递归的数据结构。
树中的某个结点(除根结点外)最多只和上一层的一个结点(即其父结点)有直接关系,根结点没有直接上层结点,因此在 n n n个结点的树中有 n − 1 n-1 n−1条边。
路径是从上向下的,同一双亲结点的两个孩子结点之间不存在路径。
路径长度是路径上所经过的边的个数。
树的基本性质:
- 树中的结点数等于所有结点的度数加1。
- 度为 m m m的树中第 i i i层上至多有 m i − 1 m^{i-1} mi−1个结点( i > = 1 i>=1 i>=1)。
- 高度为 h h h的 m m m叉树至多有 ( m h − 1 ) / ( m − 1 ) (m^h-1)/(m-1) (mh−1)/(m−1)个结点。
- 具有 n n n个结点的 m m m又树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1)\rceil ⌈logm(n(m−1)+1)⌉
注:- 树的路径长度是所有路径长度的总和,树根到每个结点的路径的最大值应是树的高度减1。
- 树中结点总数为 n n n,则 n = n= n=分支数 + 1 +1 +1,而分支数又等于树中各结点的度之和,即 n = 1 + n 1 + 2 n 2 + 3 n 3 + 4 n 4 = n 0 + n 1 + n 2 + n 3 + n 4 n=1+n_1+2n_2+3n_3+4n_4=n_0+n_1+n_2+n_3+n_4 n=1+n1+2n2+3n3+4n4=n0+n1+n2+n3+n4
- 度为2的树至少有3个结点,而二叉树可以为空
- 度为2的有序树的孩子结点的左右次序是相对于另一孩子结点而言的,若某个结点只有一个孩子结点,则这个孩子结点就无须区分其左右次序,而二又树无论其孩子数是否为2,均需确定其左右次序
- 高度为 h h h,且含有 2 h − 1 2^h-1 2h−1个结点的二叉树
- 叶子结点都集中在二叉树的最下一层,除叶子结点外的每个结点度数均为2.
- 对于编号为 i i i的结点,若有双亲,则其双亲为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋。若有左孩子,则左孩子为 2 i 2i 2i;若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1。
- 若 i < = ⌊ n / 2 ⌋ i<=\lfloor n/2\rfloor i<=⌊n/2⌋,则结点 i i i为分支结点,否则为叶子结点。
- 度为1的结点,只可能有一个,且只有左孩子.
- 出现编号为 i i i的结点为叶子结点或只有左孩子,则编号大于 i i i的结点均为叶子结点。
- n n n为奇数,则每个分支结点都有左孩子和右孩子;若 n n n为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
- 非空二叉树上的叶子结点数等于度为2的结点数加1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
- 非空二叉树上第 k k k层上至多有 2 k − 1 2^{k-1} 2k−1个结点( k > = 1 k>=1 k>=1)。
- 高度为 h h h的二叉树至多有 2 h − 1 2^h-1 2h−1个结点( h > = 1 h>=1 h>=1)。
- 完全二叉树:
1) i > 1 i>1 i>1时,双亲结点为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋, i i i为偶数,其双亲结点为 i / 2 i/2 i/2,为左孩子; i i i为奇数,其双亲结点为 ( i − 1 ) / 2 (i-1)/2 (i−1)/2,为右孩子。
2)当 2 i < = n 2i<=n 2i<=n时, 2 i 2i 2i为左孩子,当 2 i + 1 < = n 2i+1<=n 2i+1<=n时, 2 i + 1 2i+1 2i+1为右孩子
3)结点 i i i所在层次(深度)为 ⌊ l o g 2 i ⌋ + 1 \lfloor log_2i\rfloor+1 ⌊log2i⌋+1。- 具有 n n n个 ( n > 0 ) (n>0) (n>0)结点的完全二叉树的高度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1)\rceil ⌈log2(n+1)⌉或 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n\rfloor+1 ⌊log2n⌋+1。
- 用一组地址连续的存储单元,将完全二叉树上编号为 i i i的结点元素存储在某个数组下标为 i − 1 i-1 i−1的分量中
- 完全二叉树和满二叉树采用顺序存储比较合适。
- 在最坏情况下,一个高度为 h h h且只有 h h h个结点的单支树却需要占据近 2 h − 1 2^h-1 2h−1个存储单元。
注:
- 这种存储结构要从数组下标1开始存储树中的结点,若从数组下标0开始存储,则不满足计算出其孩子结点在数组中的位置
- 树的顺序存储结构中,数组下标代表结点的编号,下标上所存的内容指示了结点之间的关系。二叉树的顺序存储结构中下标既代表了结点的编号,又指示了树中各结点之间的关系
- 二叉树都可以用树的存储结构来存储,但树却不都能用二叉树的存储结构来存储。
typedef struct BiTNode{
Elemtype data;
struct BiTNode *lchild, *rchild;
}BiTNode,*BiTree;
注:
- 顺序存储空间利用率低,二叉树一般采用链式存储结构
- 含有 n n n个结点的二叉链表中,含有 n + 1 n+1 n+1个空链域
u
二叉树的遍历是指按一定规律对二叉树中的每个结点进行访问且仅访问一次。遍历操作就是将二叉树中结点按一定规律线性化的操作,目的在于将非线性化结构变成线性化的访问序列。
我们用L、D、R分别表示遍历左子树、访问根结点、遍历右子树,那么对二又树的遍历顺序就可以有六种方式:
(1)访问根,遍历左子树,遍历右子树(记做DLR)。
(2)访问根,遍历右子树,遍历左子树(记做DRL)。
(3)遍历左子树,访问根,遍历右子树(记做LDR)。
(4)遍历左子树,遍历右子树,访问根(记做LRD)。
(5)遍历右子树,访问根,遍历左子树(记做RDL)。
(6)遍历右子树,遍历左子树,访问根(记做RLD)。
二叉树的遍历运算是将二叉树中结点按一定规律线性化的过程。当以二叉链表作为存储结构时,只能找到结点的左、右孩于信息,而不能直接得到结点在遍历序列中的前驱和后继信息。要得到这些信息可采用以下两种方法:第一种方法是将二叉树遍历一遍,第二种方法是充分利用二又链表中的空链域,将遍历过程中结点的前驱、后继信息保存下来。
在有n个结点的二又链表中共有2n个链域,但只有n-1个有用的非空链域,其余n+1个链域是空的。可以利用剩下的n+1个空链域来存放遍历过程中结点的前驱和后继信息。
指向前驱和后继结点的指针叫做线索。对二义树以某种次序进行遍历并且加上线索的过程叫做线索化。
1.二叉树的线索化:线索化实质上是将二叉链表中的空指针域填上相应结点的遍历前驱或后继结点的地址,而前驱和后继的地址只能在动态的遍历过程中才能得到。因此线索化的过程即为在遍历过程中修改空指针域的过程。
2.在线索二叉树中找前驱、后继结点
(1)找结点的中序前驱结点:对于结点p,当p->Ltag=1时,p->LChild 指向p的前驱。当p->Ltag=0时.p->LChild指向p的左孩子。由中序遍历的规律可知,作为根p的前驱结点,它是中序遍历p的左子树时访问的最后一个结点,即左子树的“最右下端”结点。
(2)找结点的中序后继结点:对于结点p,若要找其后继结点,当p->Rtag=1时,p->RChild即为p的后继结点;当p->Rtag=0时,说明p有右子树,此时p的中序后继结点即为其右子树的“最左下端”的结点。
(3)先序查找结点的后继:若p 存在左子树,则p的左孩子结点为p的后继,若无左子树,有右子树,则p的右孩子结点即为p的后继。若无左右孩子,则p->RChild中所指的结点为p 的后继。
(4)先序查找结点的前驱:比较困难,若p是根,前驱为空,若p是左孩子或者双亲结点无左孩子p为右孩子,p的前驱为双亲结点。若p为右孩子,双亲结点有左孩子,则p的前驱是双亲结点左子树中按先序遍历时最后访问的结点。
3.由遍历序列确定二叉树:
先序+中序可以确定一个二叉树
分析:先序中第一个结点为二叉树的根结点,中序遍历中,根结点将中序序列分为两部分,根结点之前为左子树,根结点之后为右子树。同时,左子树和右子树的根结点又可以分别把左子树和右子树划分为两个子序列,如此递归下去可得到二叉树。
后序+中序可以确定一个二叉树
树的孩子兄弟链表结构与二叉树的二叉链表结构在物理结构上是完全相同的,只是它们的逻辑含义不同。
树的先根遍历<=>二叉树的前序遍历
树的后根遍历<=>二叉树的中序遍历
森林的先序遍历、中序历和后序遍历与其相应二又树的先序遍历、中序遍历和后序遍历是对应相同的。
1.路径:指从一个结点到另一个结点之间的分支序列
2.路径长度:指从一个结点到另一个结点所经过的分支数目。
3.结点的权:树的每个结点赋予一个具有某种实际意义的实数
4.结点的带权路径长度:从树根到某一结点的路径长度与该结点的权的乘积
5.树的带权路径长度为树中所有叶子结点的带权路径长度之和,通常记为:
WPL=∑Wi * li (1<=i<=n)
n为叶子结点的个数,Wi为第i个叶子结点的权值,li为第i个叶子结点的路径长度。
6.结点n对应的路径长度为log2n(向下取整),所以前n项之和为∑log2k(向下取整,1<=k<=n)。完全二叉树的路径长度为∑log2k(树的深度)完全二叉树具有最小路径长度的性质,但不具有唯一性。有些树并不是完全二叉树,但也可以具有最小路径长度。
7.哈夫曼树:将给定结点构成一棵(外部通路)带权树的路径长度最小的二又树。
构造哈夫曼树
哈夫曼树(最优二叉树):它是由n个带权叶子结点构成的所有二叉树中带权路径长度WPL最短的二叉树。
构造哈夫曼算法的步骤如下:
(1)初始化:用给定的n个权值{w1,w2,…,wn}对应的n个结点构成n棵二叉树的森林F={T1,T2,…,Tn),其中每一棵二叉树Ti(1<=i<=n)都只有一个权值为wi的根结点,其左、右子树为空。
(2)找最小树:在森林F中选择两棵根结点权值最小的二叉树,作为一棵新二叉树的左、右子树,标记新二叉树的根结点权值为其左右子树的根结点权值之和。
(3)删除加入:从F中删除被选中的那两棵二叉树,同时把新构成的二又树加入到森林F中。
(4)判断:重复(2)、(3)操作,直到森林中只含有一棵二叉树为止,此时得到的这棵二叉树就是哈夫曼树。
在哈夫曼树中权越大的叶子离根越近,则其具有最小带权路径长度。
哈夫曼编码
前缀编码:任意一个编码不能成为其它任意编码的前缀(最左子串)
哈夫曼编码:一棵具有n个子叶的哈夫曼树,对其左分支赋0,右分支赋1,则从根到每个叶子的通路上,各分支的赋值分别构成一个二进制串,此二进制串称为哈夫曼树。
哈夫曼编码是前缀编码。是最优前缀编码。
利用哈夫曼树可以得到平均长度最短的编码。
二叉链表结构:
typedef struct Node
{
DataType data;
struct Node * LChild;
struct Node * RChild;
}BiTNode, *BiTree;
(1) 二叉树的遍历算法
'''先序遍历二叉树'''
void PreOrder(BiTree root)
{
if(root!=NULL)
{
Visit(root->data);
PreOrder(root->LChild);
PreOrder(root->RChild);
}
}
'''中序遍历二叉树'''
void InOrder(BiTree root)
{
if(root!=NULL)
{
InOrder(root->LChild);
Visit(root->data);
InOrder(root->RChild);
}
}
'''后序遍历二叉树'''
void PostOrder(BiTree root)
{
if(root!=NULL)
{
PostOrder(root->LChild);
PostOrder(root->RChild);
Visit(root->data);
}
}
(2)遍历算法应用
'''先序遍历输出二叉树中的结点'''
void PreOrder(BiTree root)
{
if(root!=NULL)
{
printf(root->data);
PreOrder(root->LChild);
PreOrder(root->RChild);
}
}
printf(root->data);
改为:
if(root->LChild==NULL && root->RChild==NULL)
printf(root->data);
printf(root->data);
改为:
if(root->LChild==NULL && root->RChild==NULL)
LeafCount++;
b. 算法思想:采用分治算法,如果是空树,返回0;如果只有一个结点,返回1;否则为左右子树的叶子结点数之和
int leaf(BiTree root)
{
int LeafCount;
if(root==NULL) LeafCount= 0;
else if(root->LChild==NULL && root->RChild==NULL) LeafCount=1;
else{
LeafCount=leaf(root->LChild)+leaf(root->RChild);
}
return LeafCount;
}
void CreateBiTree(BiTree *bt)
{
char ch;
ch=getchar();
if(ch=='.') *bt=NULL;
else
{
*bt=(BiTree)malloc(sizeof(BiTNode));
(*bt)->data=ch;
CreateBiTree(&((*bt)->LChild));
CreateBiTree(&((*bt)->LChild));
}
}
int PostTreeDepth(BiTree bt)
{
int hl,hr,max;
if(bt==NULL) return 0;
else{
hl=PostTreeDepth(bt->LChild);
hr=PostTreeDepth(bt->RChild);
max=hl>hr?hl:hr;
}
return max+1;
}
b .前序遍历求二叉树高度
算法思想:二叉树的高度为二叉树中结点层次的最大值,遍历计算二叉树中的每个结点的层次,其中最大值即为二叉树的高度
void PreTreeDepth(BiTree bt ,int h)
{
//先序遍历求二叉树bt高度的递归算法,h为bt指向结点所在层次,初值为1
//depth 为当前求得的最大层次,为全局变量,调用前初值为0
if(bt!=NULL)
{
if(depth<h) depth=h;
PreTreeDepth(bt->LChild,h+1);
PreTreeDepth(bt->RChild,h+1);
}
}
void PrintTree(BiTree bt, int nLayer)
{
if(bt!=NULL)
{
PrintTree(bt->RChild, nLayer+1);
for(int i=0;i<nLayer;i++)
printf(' ');
printf("%c\n",bt->data);
PrintTree(bt->LChild, nLayer+1);
}
}
(3)基于栈的递归消除
在大量复杂的情况下,递归的问题无法直接转换成循环,所以需要采用工作栈消除递归。工作栈提供一种控制结构,当递归算法进层时需要将信息保留;当递归算法出层时需要从栈区退出上层信息。
'''直接实现栈操作'''
//s[m]表示栈,top表示栈顶指针
void inorder(BiTree root)
{
top=0;
do{
while(root!=NULL)
{
if(top>m) return -1;
top++;
s[top]=root;
root=root->LChild;
}
if(top!=0)
{
root=s[top];
top--;
visit(root->data);
root->RChild;
}
}while(root!=NULL||top!=0);
}
''' 调用栈操作的函数'''
void InOrder(BiTree root)
{
InitStack(&S);
p=root;
while(p!=NULL||!IsEmpty(S))
{
if(p!=NULL)
{
Push(&S,p);
p=p->LChild;
}
else{
Pop(&S, &p);
visit(p->data);
p=p->RChild;
}
}
}
递归算法的时间复杂度:n个结点二叉树,指控的次数为n+1,n为结点个数,故循环次数为n+(n+1)=2n+1,时间复杂度为O(n)
递归算法的空间复杂度:所需栈的空间最多等于二叉树深度K乘以每个结点所需空间数O(k),表面上看递归算法没有使用栈,每调用一次系统内部都有系统运行栈区支持,需要保留本层参数、临时变量与返回地址,因此递归算法比非递归算法占用的空间更多。
void PostOrder(BiTree root)
{
BiTNode *p ,*q;
Stack S;
InitStack(&S);
p=root;
q=NULL;
while(p!=NULL||!IsEmpty(S))
{
if(p!=NULL) {
Push(&S ,P); p=p->LChlid; }
else{
GetTop(&S ,&p);
if(p->RChlid=NULL||q==p->RChild)
{
visit(p->data);
q=p;
Pop(&S, &p);
p=NULL;
}
else p=p->RChild;
}
}
}
void Inthread(BiTree root)
{
if(root!=NULL)
{
Inthread(root->LChild);
if(root->LChild==NULL)
{
root->Ltag=1;root->LChild=pre;}
if(pre!=NULL&&pre->RChild==NULL)
{
pre->RChild=root;pre->Rtag=1;}
pre=root;
Inthread(root->RChild);
}
}
''' 找前驱结点'''
BiTNode * InPre(BiTNode*p)
{
if(p->Ltag==1) pre=p->LChild;
else{
for(q=p->LChild;q->Rtag==0;q=q->RChild)
;
pre=q;
}
return (pre);
}
''' 找后继结点'''
BiTNode * InNext(BiTNode*p)
{
if(p->Rtag==1) next=p->RChild;
else{
for(q=p->RChild;q->Ltag==0;q=q->LChild)
;
next=q;
}
return (next);
}
BiTNode * InFirst(BiTree Bt)
{
BiTNode *p=Bt;
if(!p) return NULL;
while(p->Ltag==0) p=p->LChild;
return p;
}
void TInOrder(BiTree Bt)
{
BiTNode *p;
p=InFirst(Bt);
while(p)
{
visit(p);
p=InNext(p);
}
}
孩子兄弟表示法的类型定义:
typedef struct CSNode
{
DataType data;
Struct CSNode * FirstChild;
Struct CSNode * Nextsibling;
}CSNode,*CSTree;
树的遍历算法
''' 先序遍历'''
void RootFirst(CSTree root)
{
if(root!=NULL)
{
visit(root->data);
p=->root->FirstChild;
while(p!=NULL)
{
RootFirst(p);
p=p->Nextsibling;
}
}
}
void RootFirst(CSTree root)
{
if(root!=NULL)
{
visit(root->data);
RootFirst(root->FirstChild);
RootFitst(root->FirstChild);
}
}