一、树的定义与基本术语
1.树的定义
树是由节点组成的有限集合,其中每个节点可以有零个或多个子节点,但只有一个父节点(除了根节点)。
根节点是树的起点,其他节点都是根节点的子节点。
叶子节点是没有子节点的节点,而分支节点具有一个或多个子节点。
2.树的相关术语
结点:包括一个数据元素及若干指向其他结点的分支信息。
结点的度:一个结点的子树个数称为此结点的度。
叶节点:度为0的结点,即无后继的结点,也称为终端结点。
分支结点:度不为0的结点,也称为非终端结点。
结点的层次:从根结点开始定义,根结点的层次为1,根的直接后继的层次为2,以此类推。
结点的层序编号:将树中的结点按从上层到下层,同层从左到右的次序排成一个线性序列,依次给它们编以连续的自然数。
树的度:树中所有结点的度的最大值。
树的高度(深度)::树中所有结点的层次的最大值。
有序树:在树T中,如果各子树之间是有先后次序的,则称为有序树。
森林:m棵互不相交的树的集合。将一棵非空树的根结点删去,树就变成一个森林;反之,给森林增加一个统一的根结点,森林就变成一棵树。
同构:对两棵树,通过对结点适当的重命名,就可以使两棵树完全相等(结点对应相等,对应结点的相关关系也相等),则称这两棵树同构。
孩子结点、双亲结点(雌雄同体)、兄弟结点…:类比人类的血缘关系。
二、二叉树
1.二叉树的定义与基本操作
定义:把满足以下两个条件的树的结构称为二叉树(Binary Tree):
i.每个结点的度都不大于2.
ii.每个结点的孩子结点次序不能任意颠倒。(区分左右孩子)
满二叉树:每层都是满的二叉树。
完全二叉树:相比满二叉树,最后一层可以缺少元素,但要求最后一层的元素都挤在左边。
2.二叉树的性质
性质1:在二叉树的第 i i i层上至多有 2 i − 1 2^{i-1} 2i−1个结点( i ≥ 0 i \ge 0 i≥0)。
当为满二叉树时,有 2 i − 1 2^{i-1} 2i−1个。
性质2:深度为 k k k的二叉树至多有 2 k − 1 2^k-1 2k−1个结点( k ≥ 0 k \ge 0 k≥0)。
等比数列求和。
性质3:对任意一棵二叉树T,若终端结点树为 n 0 n_0 n0,而其度数为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0= n_2+1 n0=n2+1。
从只有一个结点的树开始,这时 n 0 = 1 , n 2 = 0 n_0=1,n_2=0 n0=1,n2=0,在该终端结点上添加两个结点,此时终端结点数变为两个,度为2的结点有一个,即终端结点比度为2的结点多两个。进一步地,给树中的任一终端结点加两个孩子结点的结果是终端结点净增加一个,度为2的结点净增加一个,即始终有终端结点比度为2的结点多一个(注意当只给终端结点添加一个孩子结点时不改变前后终端结点和度为2的结点的数目)。
性质4:具有 n n n个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor+1 ⌊log2n⌋+1。
性质5:对完全二叉树编号为 i i i的结点:
i.若 i > 1 i>1 i>1,则其双亲结点的编号为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor ⌊2i⌋。
ii.若 2 i > n 2i>n 2i>n,则该结点无左孩子;如 2 i ≤ n 2i \le n 2i≤n,则该结点的左孩子结点的序号为 2 i 2i 2i。
iii.若 2 i + 1 > n 2i+1>n 2i+1>n,则该结点无右孩子;若 2 i + 1 ≤ n 2i+1\le n 2i+1≤n,则该结点的右孩子结点的编号为 2 i + 1 2i+1 2i+1。
3.二叉树的存储结构
(1)顺序存储结构
对于完全二叉树,可以将其数据元素逐层存放到一组连续的内存单元。但对于非完全二叉树,会造成空间上的浪费。
(2)链式存储结构
结点:左孩子域LChild, 数据域Data, 右孩子域RChild
二叉链表结点结构:
//类型定义
typedef struct Node
{
DataType data;
struct Node * LChild;
struct Node * RChild;
}BiTNode, * BiTree;
三、二叉树的遍历与线索化
1.二叉树的遍历
遍历即按一定操作,将树非线性化的结构线性化访问。
用L,D,R分别表示遍历左子树,访问根结点,遍历右子树。则二叉树的遍历顺序有六种可能:DLR,DRL,LDR,LRD,RDL,RLD。如果规定按先左后右的顺序,有DLR,LDR,LRD三种顺序。
利用递归实现如下:
//先序遍历
void DLR(BiTree root)
{
Visit(root->data);//访问结点,进行特定操作
DLR(root->LChild);
DLR(root->RChild);
}
//中序遍历
void LDR(BiTree root)
{
LDR(root->LChild);
Visit(root->data);
LDR(root->RChild);
}
//后序遍历
void LRD(BiTree root)
{
LRD(root->LChild);
LRD(root->RChild);
Visit(root->data);
}
//对每个结点执行一次入栈、一次出栈、一次访问,共n个结点,时间复杂度均为O(n)
2.遍历算法应用
(1)输出二叉树中的结点
void PRNode(BiTree root)
{
if(root != NULL)
{
printf(root->data);
DLR(root->LChild);
DLR(root->RChild);
}
}
(2)输出二叉树中的叶子结点
void PRLeaf(BiTree root)
{
if(root != NULL)
{
if(root->LChild == NULL && root->RChild == NULL)//判断叶子结点
{
printf(root->data);
}
DLR(root->LChild);
DLR(root->RChild);
}
}
(3)统计叶子结点数目
//1.全局变量leafcount保存叶子结点,调用前置为0
void CountLeaf(BiTree root)
{
if(root != NULL)
{
if(root->LChild == NULL && root->RChild == NULL)
{
leafcount++;
}
CountLeaf(root->LChild);
CountLeaf(root->RChild);
}
}
//2.分治算法
int CountLeaf(BiTree root)
{
int leafcount;
if(root == NULL)
leafcount = 0;
else if(root->LChild == NULL && root->RChild == NULL)
leafcount = 1;
else
leafcount = CountLeaf(root->LChild) + CountLeaf(root->RChild);//后序遍历,在该结点的CountLeaf中,先对左右子树进行CountLeaf
return leafcount;
}
(4)建立二叉链表方式存储的二叉树
扩展先序遍历序列,AB . DF … G … C . E . H …, 用 . 表示空子树。
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)->RChild));
}
}
(5)求二叉树的深度
二叉深度的递归定义:
1.若bt为空,则树的深度为0.
2.若bt非空,其高度应为其左右子树高度的最大值加1.
int PostTreeDepth(BiTree bt)
{
int hl, hr, max;
if(bt != NULL)
{
hl = PostTreeDepth(root->LChild);
hr = PostTreeDepth(root->RChild);
max = hl > hr ? hl : hr; //求左右子树的深度的最大值
return max + 1;
}
else return 0;//如果是空树,返回0
}
//先序遍历
void PreTreeDepth(BiTree bt, int h)
{
if(bt != NULL)
{
if(h > depth) depth = h;//depth为全局变量,保存树的深度
PreTreeDepth(bt->LChild, h + 1);
PreTreeDepth(bt->RChild, h + 1);
}
}
(6)按树状打印二叉树
把树放倒,从上往下打印,恰为逆中序RDL。
void PrintTree(BiTree bt, int n)//n为结点的深度
{
int i;
if(bt != NULL)
{
PrintTree(bt->RChild, n + 1);
for(i = 0; i < n; i ++) printf(" ");//打印n个空格,格式好看,反映层次关系
printf("%c\n", bt->data);
PrintTree(bt->LChild, n + 1);
}
}
3.基于栈的递归消除
(1)中序遍历二叉树的非递归算法
//算法1.结构并不好
void InOrder(BiTree root)
{
int top = -1;
L1: if(p != NULL)
{
top = top + 2;
if(top > m) retrun; //栈满溢出
s[top - 1] = p; //根结点入栈
s[top] = L2; //返回地址进栈
p = p->LChild; //给下层参数赋值
goto L1; //转向开始
L2: Visit(p->data);
top = top + 2;
if(top > m) return; //栈满溢出处理
s[top - 1] = p; //遍历右子树
s[top] = L3;
p = p->RChild;
goto L1;
}
L3: if(top != 0)
{
addr = s[top];
p = s[top - 1]; //取出返回地址
top = top - 2; //退出本层参数
goto addr;
}
}
//2.直接实现栈操作
void Inorder(BiTree root)
{
top = 0;
p = root;
do
{
while(p != NULL)//遍历左子树
{
if(top > m) retrun;
top = top + 1;
s[top] = p;
p = p->LChild;
}
if(top != 0)
{
p = s[top];
top = top - 1;
Visit(p->data);//访问根结点
p = p->RChild;//遍历右子树
}
}while(p != NULL || top != 0);
}
//3.调用栈操作的函数
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;
}
}
}
(2)后序遍历二叉树的非递归算法
void PostOrder(BiTree root)
{
BiTree * p, * q;
Stack S;
q = NULL;
p = root;
InitStack(&S);
while(p != NULL || !IsEmpty(S))
{
if(p != NULL)
{
Push(&S, p);
p = p->LChild;
}
else
{
GetTop(&S, &p);
if(p -> RChild != NULL || p-> Rchild == q)//无右子树,或右子树已经访问过
{
Visit(p->data);
q = p;//记录访问过的结点
Pop(&S, &p);
p = NULL;
}
else
{
p = p->RChild;
}
}
}
}
4.线索二叉树
(1)基本概念
之前的遍历只能得到结点的左右孩子信息,而不能直接得到结点在遍历序列中的前驱和后继信息。要得到前驱和后继信息,可以充分利用二叉链表终的空链域,将遍历过程中的前驱和后继信息保留下来。
对有 n n n个结点的二叉链表,共有 2 n 2n 2n个链域, 但非满结点(叶子结点或度为1的结点)的 n + 1 n+1 n+1个链域是空的,没有充分利用起来。回顾之前的遍历过程,可以发现,无论是先序,中序还是后序,前驱和后继信息的缺失就发生在非满结点处。利用非满结点的链域储存前驱后继,使二叉树的线性化更完备。
因此,为了统一性和充分利用空间,给结点的结构增加两个标志域:
typedef struct Node
{
DataType data;
int Ltag;//Ltag = 0表示LChild指向左孩子,Ltag = 1表示指向前驱
struct Node * LChild;
int Rtag;//Rtag = 0表示RChild指向右孩子,Rtag = 1表示指向后继
struct Node * RChild;
}BiTNode, * BiTree;
(2)二叉树的线索化
线索化的过程即为在遍历过程中修改空指针的过程。按不同的遍历次序,可以得到先序二叉树、中序二叉树和后序二叉树。
以中序线索二叉树为例:
//建立中序线索二叉树
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->Rtag = 1;
pre->RChild = root;
}
pre = root;//当前结点是下一个结点的前驱
Inthread(root->RChild);
}
}
(3)找前驱、后继结点
// 1.找结点的中序前驱结点
BiTree *InPre(BiTree *p)
{
if (p->Ltag == 1)
pre = p->LChild; // 直接利用线索
else // 找左子树的最右下端结点,即为中序前驱
{
q = p->LChild;
while (q->Rtag == 0)
{
q = q->RChild;
pre = q;
}
}
return pre;
}
// 2.找结点的中序后继结点
BiTree *InNext(BiTree *p)
{
if (p->Rtag == 1)
next = p->RChild; // 直接利用线索
else // 找右子树的最左下端结点,即为中序后继
{
q = p->RChild;
while (q->Ltag == 0)
{
q = q->LChild;
next = q;
}
}
return next;
}
(4)遍历中序线索树
//1.在中序线索树上求中序遍历的第一个结点
BiTree * InFirst(BiTree Bt)
{
BiTNode * p = Bt;
if(!p) return (NULL);
while(p->Ltag == 0) p = p->LChild;
return p;
}
//2.遍历中序线索二叉树
void TInOrder(BiTree Bt)
{
BiTNode * p;
p = InFirst(Bt);
while(p)
{
visit(p);
p = InNext(p);
}
}
四、树、森林和二叉树的关系
1.树的存储结构
(1)双亲表示法
#define MAX 100
typedef struct TNode
{
DataType data;
int parent;//指示双亲结点
}TNode;
typedef struct
{
TNode tree[MAX];
int nodenum;
}ParentTree;
(2)孩子表示法
以顺序表作为存储结构,各个节点依次存储在顺序表中。
typedef struct ChildNode //孩子链表结点
{
int Child; //该孩子结点在线性表中的位置
struct ChildNode * next; //指向下一个孩子结点的指针
}ChildNode;
typedef struct //顺序表结点
{
DataType data; //结点的信息
ChildNode * FirstChild; //指向孩子链表的头指针,可以为空
}DataNode;
typedef struct //树
{
DataNode nodes[MAX]; //顺序表
int root; //该树的根结点在线性表中的位置
int num; //该树的结点个数
}ChildTree;
(3)孩子兄弟表示
以二叉链表为存储结构,每个结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟(sibling)结点。
typedef struct CSNode
{
DataType data;
struct CSNode * FirstChild;
struct CSNode * Nextsibling;
}CSNode, * CSTree;
2.树、森林与二叉树的相互转换
(1)树转换为二叉树
兄弟跟着大哥混
(2)森林转换为二叉树
森林里的树是兄弟
(3)二叉树还原为树或森林
3.树与森林的遍历
(1)树的遍历
void RootFirst(CSTree root)
{
if(root != NULL)
{
Visit(root->data);
RootFirst(root->FirstChild);
RootFirst(root->Nextsibling);
}
}
(2)森林的遍历
从第一棵树开始,依次先序或中序或后序遍历每棵树。
五、哈夫曼树及其应用
1.哈夫曼树
(1)哈夫曼树的基本概念
将给定结点构成一棵带权树的路径长度最小的二叉树
(2)构造哈夫曼树
选择权小的结点放在树的较深层。、
初始化:所有结点构成森林F。
找最小树并合并:在F中选择两棵根结点权值最小的二叉树,合并为一棵二叉树,新的根节点的权值为两个权值之和。新树加入森林。
循环:直到剩下一棵二叉树。
(3)哈夫曼树的类型定义
#define N 20 //叶子结点的最大值
#define M 2 * N - 1 //所有结点的最大值
typedef struct
{
int weight;
int parent;
int LChild;
int RChild;
}HTNode, HuffmanTree[M+1];//0号单元不用
(4)哈夫曼树的算法实现
void CrtHuffmanTree(HuffmanTree ht, int w[], int n)
{
for(i = 1; i <= n; i ++)//初始化叶子结点
ht[i] = {w[i], 0, 0, 0};
m = 2 * n + 1;
for(i = n + 1; i<= m; i ++)//初始化非叶节点
ht[i] = {0, 0, 0, 0};
for(i = n + 1; i <= m; i ++)
{
select(ht, i - 1, &s1, &s2); //选weight最小的结点s1,s2
ht[i].weight = h[s1].weight + ht[s2].weight;
ht[s1].parent = i;
ht[s2].parent = i;
ht[i].LChild = s1;
ht[i].RChild = s2;
}
}
2.哈夫曼编码
(1)哈夫曼编码的概念
为了缩短数据文件的长度,可采用不定长编码,给使用频度高的字符编以较短的编码。
前缀编码:任意编码都不是其余编码的前缀,不会产生二义性。
哈夫曼编码:给哈夫曼树的每个左分支赋予0,右分支赋予1,从根到叶子的通路,构成的二进制串。
哈夫曼编码是前缀编码,因为不同的两个编码之间必然会有分叉,分叉的左右路径为0和1,必然不同。其中一个编码不能用另外一个的前缀表示。
哈夫曼编码是最优前缀编码。
(2)哈夫曼编码的算法实现
void CrtHuffmanCode(HaffmanTree ht, HuffmanCode hc, int n)
{
char * cd;
cd = (char *)malloc(n * sizeof(char));
cd[n - 1] = '\0';//存放编码结束符
for(i = 1; i <= n; i ++)
{
start = n - 1;
c = i;
p = ht[i].parent;
while(p != 0)
{
start --;
if(ht[p].LChild == c) cd[start] = '0';//左分支0
else cd[start] = '1';//右分支1
c = p;
p = ht[p].parent; //像上倒推
}
hc[i] = (char * )malloc((n - start) * sizeof(char));
strcpy(hc[i], &cd[start]);
}
free(cd);
}