定义
n(n≥0)个结点的有限集合。n=0时,称为空树。任意一棵非空树满足以下条件:
⑴ 有且仅有一个特定的称为根(root)的结点;
⑵ 当n>1时,除根结点之外的其余结点被分成m(m>0)个 互不相交的有限集合 T1, T2, … , Tm,其中 每个集合又是一棵树,并称为该根结点的子树。
树的存储结构
1. 双亲表示法
用一维数组来存储树的各个结点(一般按层序存储),数组中的一个元素对应树中的一个结点,该元素包含结点的数据信息以及该结点的父结点下标。
typedef struct tnode{
datatype data;
int parent;
}tree[n]
2. 孩子表示法
树中结点可能含有多个孩子,一般采用多重链表来表示,链表中的每个结点都包括一个数据域和多个指针域,每个指针域指向该结点的一个孩子结点。
a. 同构:按结点的度的最大值(即树的度)来设置指针域数量,即1个数据域和d(树的度)个指针域。
b. 异构:按结点各自的度设置指针域数量,即1个数据域和d(结点的度)个指针域。
c. 综合a和b两种方法:把每个结点的孩子结点排列起来,看成一个线性表,用单链表存储。那么n个结点共有n个孩子链表。每个孩子链表都会有一个头指针,n个单链表共有n个头指针,n个头指针又可以组成一个线性表,可以用顺序存储结构。
顺序表childlink:
| data_A, h00 | data_B, h10 | data_C, h11 | data_D, h20 | data_E, h21 | data_F, h22 |data_G, h23 | data_H, h24 | data_I, h30 | data_J, h31 |
单链表:
h00->B->C
h10->D->E->F, h11->G->H
h20->NULL, h21->I->J, h22->NULL, h23->NULL, h24->NULL
h30->NULL, h31->NULL
typedef struct tagnode{ /*孩子结点,组成单链表*/
int child;
struct tagnode *next;
}node, *link;
typedef struct{ /*头结点*/
datatype data;
link firstchild;
}headnode;
typedef headnode childlink[maxnode]; /*表头数组*/
3. 孩子兄弟表示法
以二叉链表作为存储结构,结点的两个指针域分别指向该结点的第一个孩子结点和右边的兄弟结点。
![在这里插入图片描述]
typedef struct treenode{
datatype data;
struct treenode *firstchild, *rightsib;
}treenode, *tree;
二叉树
性质
(1)一棵非空二叉树的第i层上最多有2i-1个结点(i≥1)。
(2)一棵深度为k的二叉树中,最多具有2k-1个结点。
(3)对于一棵非空的二叉树,设叶子结点数为n0,度为2的结点数为n2,则有n0=n2+1。
证明:设n为二叉树的结点总数,n1为二叉树中度为1的结点数,则有:n=n0+n1+n2。在二叉树中,除根结点外,其余结点都有双亲,所以这些结点都有唯一的一个分支来指向。设B为二叉树中的分支数,那么有:B=n-1。B所代表的这些分支都是由度为1和度为2的结点发出的,一个度为1的结点发出1个分支,一个度为2的结点发出2个分支,所以有:B=n1+2n2 。
综上所述,可以得到:n0=n2+1
(4)在有n个结点的满二叉树中,叶子结点n0 = (n + 1) / 2
完全二叉树
除最后一个叶子结点的父结点外,其他父结点的度数都为2。
(5)具有n个结点的完全二叉树的深度k = log2n + 1。
证明:当一棵完全二叉树的深度为k、结点个数为n时,有 2k-1 - 1 < n ≤ 2k - 1,即 2k-1 ≤ n < 2k。对该不等式取对数,有 k - 1 ≤ log2n < k,即 log2n < k ≤ log2n + 1,由于k是整数,所以有k=log2n + 1。
(6)对于具有 n 个结点的完全二叉树,如果按照从上往下和从左至右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为 i 的结点,有:
a. 如果 i > 1,则序号为 i 的结点的双亲结点的序号为 i / 2;如果 i=1,则序号为 i 的结点是根结点,无双亲结点。
b. 如果 2i ≤ n,则序号为 i 的结点的左孩子结点的序号为 2i;如果 2i>n,则序号为i的结点无左孩子。
c. 如果 2i+1 ≤ n,则序号为 i 的结点的右孩子结点的序号为 2i+1;如果 2i+1>n,则序号为i的结点无右孩子。
顺序存储结构
用一组连续的存储单元来存放二叉树中的结点,存储二叉树结点时一般按照从上往下、从左至右的顺序存储。
完全二叉树和满二叉树采用顺序存储比较合适,因为根据性质(6)可知,完全二叉树和满二叉树都可以通过结点的序号来找到它的双亲和孩子,唯一地反映出结点之间的逻辑关系。
对于一般的二叉树,数组元素下标之间的关系并不能够反映出二叉树中结点之
间的逻辑关系,此时必须增加一些并不存在的虚结点,使之演变成为一棵完全二叉树的形式。
一般二叉树 完全二叉树
链式存储结构
采用链表的方式来表示一棵二叉树,元素间的逻辑关系可以通过链结点的指针来指示。
- 二叉链表存储方式
typedef struct BTNode{
datatype data;
struct BTNode *lchild,*rchild; /*左右孩子指针*/
}BTNode,*BiTree;
建立二叉树
BiTree Create(datatype root, BiTree lbt, BiTree rbt){ /*生成一棵以root为根结点的数据域值,以lbt和rbt为左、右子树的二叉树*/
BiTree p;
if((p = (BTNode *)malloc(sizeof(BTNode))) == NULL) return NULL;
p->data = root;
p->lchild = lbt;
p->rchild = rbt;
return p;
}
作为做孩子结点插入二叉树
BiTreeInsertL(BiTree bt, datatype e, BiTree par){ /*在二叉树bt的结点par的左子树插入结点数据元素*/
BiTree p;
if(par == NULL){
printf("\n插入错误");
return NULL;
}
if((p = (BTNode *)malloc(sizeof(BTNode))) == NULL) return NULL;
p->data = e;
p->lchild = NULL;
p->rchild = NULL;
if(par -> lchild == NULL) par->lchild = p;
else{
p->lchild = par->lchild = p;
par->lchild = p;
}
return bt;
}
- 三叉链表存储方式
二叉树的遍历
二叉树的遍历是指按照给定的某种顺序来依次访问二叉树中的每个结点,使每个结点被访问一次且仅被访问一次。
前序遍历:A B C D E F G H K
中序遍历:B D C A H G K F E
后序遍历:D C B H K G F E A
层次遍历:A B E C F D G H K
层次遍历按层次顺序,从二叉树的根结点开始,从上往下逐层遍历;在同一层中,从左至右的对结点访问。符合“先进先出”的特性,该特性与队列的操作特性吻合。
遍历时先将根结点指针入队,然后从队列中每取一个元素,执行下述两步操作:
(1)访问该元素所指结点;
(2)若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子和右孩子结点顺序入队。
重复上述两步,直至队列为空,二叉树的层次遍历结束。
LevelOrder(BiTree bt){
BiTree Que[MAXSIZE];
int front, rear;
if (bt == NULL) return 1;
front = -1;
rear = 0;
Que[rear] = bt;
while(front != rear){ /*队列不为空*/
front++;
Visite(Que[front]->data); /*访问队首结点的数据域*/
if (Que[front]->lchild != NULL){ /*将队首结点的左孩子结点入队列*/
rear++;
Que[rear] = Que[front]->lchild;
}
if (Que[front]->rchild != NULL){ /*将队首结点的右孩子结点入队列*/
rear++;
Que[rear] = Que[front]->rchild;
}
}
}
线索二叉树
一个具有n个结点的二叉树若采用二叉链表存储结构,必有n+1个指针域存放的都是NULL。
若某结点的左孩子指针域(lchild)为空,利用它来指出该结点在某种遍历序列中的直接前驱结点的存储地址,若某结点的右孩子指针域(rchild)为空,利用它来指出该结点在某种遍历序列中的直接后继结点的存储地址;非空的指针域,仍存放指向该结点左、右孩子的指针。
为每个结点增设两个标志位域ltag和rtag,令:
l t a g = { 0 l c h i l d 指 向 结 点 的 左 孩 子 1 l c h i l d 指 向 结 点 的 前 驱 结 点 ltag =\begin{cases} 0&lchild 指向结点的左孩子\\1&lchild指向结点的前驱结点\end{cases} ltag={01lchild指向结点的左孩子lchild指向结点的前驱结点
r
t
a
g
=
{
0
r
c
h
i
l
d
指
向
结
点
的
右
孩
子
1
r
c
h
i
l
d
指
向
结
点
的
后
继
结
点
rtag =\begin{cases} 0&rchild 指向结点的右孩子\\1&rchild 指向结点的后继结点\end{cases}
rtag={01rchild指向结点的右孩子rchild指向结点的后继结点
- 前序线索二叉树
- 中序线索二叉树
- 后序线索二叉树
1. 结点定义
typedef struct BiThrNode{
daattype data;
struct BiThrNode *lchild;
sturct BiThrNode *rchild;
unsigned ltag:1;
unsigned rtag:1;
}BiThrNode, *BiThrTree;
2. 中序线索二叉树的建立
void InThread (BiThrTree p)
{ /*中序遍历过程中进行中序线索化*/
if (p)
{ InThread (p->lchild); /*左子树线索化*/
if (!p->lchild) /*建立p的前驱线索*/
{ p->ltag=1; p->lchild=pre;
}
if (!pre->rchild) /*建立pre的后继线索*/
{ pre->rtag=1; pre->rchild=p;
}
pre=p; /* 确保pre恒指向前驱*/
InThread (p->rchild); /*右子树线索化*/
}
3. 查找中序前驱结点
两种情况:
(1) 如果该结点无左子树,那么它的左标志ltag值为1,则其左孩子指针域lchild所指向的结点便是它的前驱结点;
(2) 如果该结点有左子树,那么它的左标志ltag值为0,则其左孩子指针域lchild所指向的结点是它的左孩子。
BiThrTree InPreNode(BiThrTree p){/*在中序线索二叉树上寻找结点p的中序前驱结点*/
BiThrTree pre;
pre = p->lchild;
if (p->ltag != 1) /*左子树存在*/
while (pre->rtag == 0)
pre = pre->rchild; /*寻找最右结点,因为p的左子树的最右结点是p的直接前驱结点*/
return pre ;
}
4. 查找中序后继结点
两种情况:
(1) 如果该结点无右子树,那么它的右标志rtag值为1,则其右孩子指针域rchild所指向的结点便是它的后继结点;
(2) 如果该结点有右子树,那么它的右标志rtag值为0,则其右孩子指针域rchild所指向的结点便是它的右孩子 。
BiThrTree InPostNode(BiThrTree p){/*在中序线索二叉树上寻找结点p的中序后继结点*/
BiThrTree post;
post = p->rchild;
if(p->rtag != 1) /*右子树存在*/
while(post->ltag == 0)
post = post->lchild; /*寻找最左结点,因为p的右子树的最左结点是p的直接后继结点*/
return post ;
}
5. 查找值为e的结点
先找到按中序遍历的第一个结点,然后再一次搜索其直接后继结点;或先找到按中序遍历的最后一个结点,然后一次搜索其直接前驱结点。
BiThrTree Locate(BiThrTree H, datatype e){ /*在以H为头结点的中序线索二叉树中查找值为e的结点*/
BiThrTree p;
p = H->lchild;
while(p->ltag == 0 && p != H) p = p->lchild; /*找到遍历的第一个结点*/
while(p != H && p->data != e) p = InPostNode(p); /*查找后继结点*/
if(p == H){
printf("Not found the data!\n");
return 0;
}
else return p;
}
树转换成二叉树
1. 转换步骤
⑴ 加线:树中所有相邻兄弟之间加一条连线。
⑵ 去线:对树中的每个结点,只保留它与第一个孩子结点之间的连线,删去它与其它孩子结点之间的连线。
⑶ 层次调整:以根结点为轴心,将树顺时针转动45度,使之层次分明。
由树转换成的对应二叉树其特点都是没有右子树的。
树的前序遍历序列:ABEFCGDHIJ
二叉树的前序遍历序列: ABEFCGDHIJ
树的后序遍历序列: EFBGCHIJDA
二叉树的中序遍历序列:EFBGCHIJDA
2. 结论
树的前序遍历序列与对应二叉树的前序遍历序列相同,树的后序遍历序列与对应二叉树的中序遍历序列相同。
森林转换为二叉树
1. 转换步骤
⑴ 将森林中的每棵树依上述方法转换成二叉树,此时的二叉树都没有右子树;
⑵ 从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子来处理,当所有二叉树依上法连起来后所得到的那一棵二叉树就是由森林转换得到的二叉树。
2. 结论
森林的前序遍历序列与对应二叉树的前序遍历序列相同,森林的中序遍历序列与对应二叉树的中序遍历序列相同。
二叉树算法的应用
1. 查找数据元素
BiTree Locate(BiTre bt, datatype e){ /*在以bt为根结点指针的二叉树中查找数据元素e*/
BiTree p;
if(bt->data == e) return bt; /*查找成功后返回*/
if(bt->lchild != NULL) return(Locate(bt->lchild, e)); /*在以bt->lchild为根结点指针的二叉树中查找数据元素e*/
if(bt->rchild != NULL) return(Locate(bt->rchild, e)); /*在以bt->rchild为根结点指针的二叉树中查找数据元素*/
return NULL; /**查找失败后返回/
}
2. 显示二叉树
void printtree(BiTree bt, int n){ /*从第n层开始中序遍历分层显示二叉树bt*/
int i;
if(bt == NULL) return 1;
printtree(bt->lchild, n+1); /*中序遍历屏幕显示(n+1)层二叉树bt->lchild*/
for(i=0; i<n-1; ++i) /*光标移过前n-1层*/
printf(" ");
if(n >= 1) print("---"); /*显示第n层连接线*/
printf("%d\n", bt->data); /*显示第n层数据域值*/
printree(bt->rchild, n+1); /*中序遍历,屏幕显示(n+1)层二叉树bt->rchild*/
}
3. 统计叶子结点数目
int CountLeaf(BiTree bt){ /*以bt为根结点所在结点的指针,返回值为bt的叶子结点数*/
int count;
if(bt == NULL) return 0;
if(bt->lchild == NULL && bt->rchild == NULL) return 1;
count = CountLeaf(bt->lchild) + CountLeaf(bt->rchild);
return count;
}
4. 求二叉树深度
int Depth(BiTree bt){ /*返回二叉树bt的深度*/
int depthBitree;
if(!bt) depthBitree = 0;
else{
depthLeft = Depth(bt->lchild); /*递归求左子树的深度*/
depthRight = Depth(bt->rchild); /*递归求右子树的深度*/
depthBitree = 1 + (depthLeft > depthRight ? depthLeft: depthRight);
}
return depthBitree;
}
5. 创建二叉树
二叉树的创建同样可以参考遍历算法来实现。若以前序遍历来创建二叉树,则可以按二叉树的前序遍历序列次序输入结点值。如果某结点无左孩子结点或右孩子结点,那么对应它的左孩子结点或右孩子结点要以特殊符号“#”表示。
void CreateBinTree(BiTree bt){ /*按加入结点的前序遍历序列输入,构造二叉链表*/
char ch;
scanf("\n%c", &ch);
if(ch == '#') bt = NULL; /*读入#时,将相应结点置空*/
else{
bt = (BTNode *)malloc(sizeof(BTNode)); /*生成结点空间*/
bt->data = ch;
CreateBinTree(bt->lchild); /*构造二叉树的左子树*/
CreateBinTree(bt->rchild); /*构造二叉树的右子树*/
}
}
最优二叉树——哈夫曼树
给定n个权值作为n个叶子结点,构造一棵二叉树,若该棵二叉树带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。
1. 基本概念
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或子孙结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
结点的权:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
如果二叉树具有n个带权值的叶结点,那么从根结点到各个叶结点的路径长度与相应叶结点权值的乘积之和叫做二叉树的带权路径长度,记为:
W
P
L
=
∑
k
=
1
n
W
k
⋅
L
k
WPL=\sum_{k=1}^n W_k\cdot L_k
WPL=k=1∑nWk⋅Lk
WPL=2×3+4×3+6×2+8×1=38
一棵二叉树如果想要使其WPL值最小,必须使权值越大的叶结点越靠近根结点,从而缩短它的路径长度,而权值越小的叶结点越远离根结点,虽然路径长度增加了,但因为结点权值小,对总的带权路径长度的影响不大。
2. 哈夫曼树的构造算法
(1)由给定的n个权值{w1,w2,…,wn}构造n棵只有一个结点的二叉树,该结点既是根结点,也是叶子结点,从而得到一个二叉树的集合F={T1,T2,…,Tn};
(2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树(不约定顺序)构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和;
(3)在集合F中删除作为左、右子树的两棵二叉树,并将新构造的二叉树加入到集合F中,F中二叉树的数量减少了一棵;
(4)重复(2)(3)两步,当F中最终只剩下一棵二叉树时,这棵二叉树便是所要构造的哈夫曼树。
设置一个结构体数组HuffmanNode保存哈夫曼树中各结点的信息 ,大小设置为2n-1 ,数组元素的结构如下:
weight域保存该结点的权值;
lchild和rchild域分别保存该结点的左、右孩子结点在数组中的序号;
parent域保存其双亲结点在数组中的序号,某结点的parent域的值不是-1,就表示该结点已加入哈夫曼树 ;
#define MAXWeight 10000 /*定义最大权值*/
#define MAXLeaf 40 /*定义哈夫曼树中叶子结点的个数*/
#define MAXNode MAXLeaf*2-1
typedef struct{
int weight;
int parent;
int lchild;
int rchild;
}HNode;
void HuffmanTree(HNode HuffmanNode [ ]){ /*哈夫曼树的构造算法*/
int i,j,a1,a2,b1,b2,n;
scanf(“%d”,&n); /*输入叶子结点个数*/
for(i = 0; i < 2 * n - 1; i++){ /*初始化数组HuffmanNode[]*/
HuffmanNode[i].weight = 0;
HuffmanNode[i].parent = -1;
HuffmanNode[i].lchild = -1;
HuffmanNode[i].rchild = -1;
}
for(i = 0; i < n; i++) scanf(“%d”, &HuffmanNode[i].weight); /*输入n个叶子结点的权值*/
for(i = 0; i < n - 1; i++){ /*构造哈夫曼树*/
a1 = a2 = MAXWeight;
b1 = b2 = 0;
for(j = 0; j < n + i; j++){
if(HuffmanNode[j].weight<a1 && HuffmanNode[j].parent == -1){
a2 = a1;
b2 = b1;
a1 = HuffmanNode[j].weight;
b1 = j;
}
else if(HuffmanNode[j].weight<a2 && HuffmanNode[j].parent == -1){
a2 = HuffmanNode[j].weight;
b2 = j;
}
}
/*将找出的2棵二叉树合并为1棵二叉树,找出的2棵二叉树作为新二叉树的左右子树*/
HuffmanNode[b1].parent = n + i;
HuffmanNode[b2].parent = n + i;
HuffmanNode[n + i].weight = HuffmanNode[b1].weight + HuffmanNode[b2].weight;
HuffmanNode[n + i].lchild = b1;
HuffmanNode[n + i].rchild = b2;
}
}