二叉树
定义:二叉树是n(n≥0)个结点的有限集,它或为空树(n=0),或由一个根结点和两棵分别称为左子树和右子树的互不相交的二叉树构成
特点:每个结点至多有二棵子树(即不存在度大于2的结点),二叉树的子树有左、右之分,且其次序不能任意颠倒。
注意区分二叉树、树、度为2的有序树:
度值的区别:二叉树的度不超过2,但不一定是2。对于二叉树的子树而言,要么是根的左子树,要么是根的右子树,即使只有一棵子树也要区分是左是右。度为2的有序树中,当一个结点有两棵子树时有左右之分,而只有一棵子树时就无左右之分。
性质:
1.若二叉树的层次从i开始,则在二叉树的第i层最多有个结点。(i>1)
2.高度为k的二叉树最多有个结点。(k≥1)
3.对任何一棵二叉树,如果其叶结点个数为n,度为2的非叶结点个数为,则有
。
4.具有n个结点的完全二叉树的高度为⌊⌋+1。
5.对于具有n个结点的完全二叉树,如果按照从上到下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为i的结点有:
(1)若i=1,则i无双亲结点,若i>1,则i的双亲结点为i/2
(2)若2*i>n,则i无左孩子,若2*i<n,则i结点的左孩子结点为2*i
(3)若2*i+1 >n,则i无右孩子,若2*i+1≤n,则i的右孩子结点为2*i+1
两种特殊的二叉树:
[满二叉树] 深度为k且有个结点的二叉树
满二叉树的特点是每一层上的结点数都最大,只有度为0和度为2的结点,每一个结点均有两棵高度相同的子树,叶子结点都在树的最下面的同一层上。
[完全二叉树] 每个结点都与等高的满二叉树中结点层序编号一一对应的二叉树
深度为k的完全二叉树在k-1层上一定是满二叉树。
叶子结点只能出现在最下两层,且最下层的叶子结点都集中在二叉树的左部。
完全二叉树中如果有度为1的结点,只可能有一个,且该结点只有左孩子。
通俗来说,完全二叉树从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐。满二叉树必为完全二叉树,而完全二叉树不一定是满二叉树。
二叉树的储存结构
[顺序存储]
用一组地址连续的存储单元存储完全二叉树的数据元素。其中编号(满二叉树层序编号为i 的结点元素存放在一维数组的下标为i-1的分量中。
#define MAX_TREE_SIZE 100//二叉树的最大结点数
typedef TElemType SqBiTree[MAX_TREE_SIZE];//0号单元储存根节点
SqBiTree bt;
一般二叉树则仿照完全二叉树那样存储。与等高完全二叉树比较,“不存在”的结点元素在数组中存放特殊值。
这种顺序存储结构适合于完全二叉树,而对一般二叉树则可能造成存储空间浪费。
[链式储存]
表示二叉树的链表中的结点至少包含三个域:数据域和左、右指针域。
1.二叉链表
typedef struct BiTNode//结点结构
{
TElemTypedata;
struct BiTNode *lchild, *rchild;//左右孩子指针
}BiTNode, *BiTree;
在n个结点的二叉链表中,有2n个指针域。
在n个结点的二叉链表中,有n+1个空指针域。
2.三叉链表
为了便于找到双亲结点,可以增加一个Parent域,以指向该结点的双亲结点。
typedef struct TriTNode
{
TElemType data;
struct TriTNode *lchild,*rchild,*parent;
}TriTNode,*TriTree;
二叉树的基础操作
[二叉树的建立]
void CreateBiTree(BiTree &T) //先序建立二叉树
{
Elemtype ch;
scanf(&ch);
if(ch=='#') T=NULL;
else
{
T=(BiTree)malloc(sizeof(BiNode));
T->data=ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
}
例如: ABC##DE#G##F###
[遍历二叉树]
二叉树的遍历是指从根结点出发,按照某种次序访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
遍历方法:先序遍历、中序遍历、后序遍历、层序遍历
先序遍历(DLR) | 中序遍历(LDR) | 后序遍历(LRD) |
若二叉树为空,则空操作。 否则: (1)访问根结点(D); (2)先序遍历左子树(L); (3)先序遍历右子树(R)。 | 若二叉树为空,则空操作。 否则: (1)中序遍历左子树(L); (2)访问根结点(D); (3)中序遍历右子树(R)。 | 若二叉树为空,则空操作。 否则: (1)后序遍历左子树(L); (2)后序遍历右子树(R); (3)访问根结点(D)。 |
“先、中、后”决定根节点的遍历顺序。
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//先序遍历
void PreOrder(BiTree T)
{
if (T)//递归出口
{
printf(T->data);//访问根节点
PreOrder(T->lchild);//递归遍历左子树
PreOrder(T->rchild);//递归遍历右子树
}
}
//中序遍历
void InOrderTraverse(BiTree T)
{
if(T)
{
InOrderTraverse(T->lchild);
printf(T->data);
InOrderTraverse(T->rchild);
}
}
//后序遍历
void PostOrderTraverse(BiTree T)
{
if(T)
{
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
printf(T->data);
}
}
对于先序、中序、后序遍历序列中(各结点值不同):
由先序和中序遍历结果可以唯一确定一棵二叉树。
由后序和中序遍历结果可以唯一确定一棵二叉树。
由先序和后序遍历结果不能唯一确定一棵二叉树。
若一棵二叉树的先序、中序序列相同,则该二叉树的形态为:空树、只有根结点、右单支
若一棵二叉树的后序、中序序列相同,则该二叉树的形态为:空树、只有根结点、左单支
若一棵二叉树的先序、后序序列相同,则该二叉树的形态为:空树、只有根结点
计算二叉树结点总数:如果是空树,则结点个数为0。否则,结点个数为左子树的结点个数+右子树的结点个数再+1。
int NodeCount(BiTree T)
{
if(T==NULL) return 0;
else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
计算二叉树叶子结点总数:如果是空树,则叶子结点个数为0;如果左右子树均为空,则叶子结点个数为1;否则,为左子树的叶子结点个数+右子树的叶子结点个数。
int LeadCount(BiTree T)
{
if(T==NULL) return 0;//如果是空树返回0
if(T->lchild==NULL && T->rchild==NULL) return 1;//如果是叶子结点返回1
else return LeafCount(T->lchild)+LeafCount(T->rchild);
}
计算二叉树深度:如果是空树,则深度为0;否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1。
int Depth(BiTree T)
{
if(T==NULL) return 0;
else
{
m=Depth(T->lchild);
n=Depth(T->rchild);
if(m>n) return (m+1);
else return (n+1);
}
}
结点x的查找:
BiTree search(BiTree T, ElemType x)
{
if(T==NULL) return NULL;
else
{
if(T->data==x) return T;
if(p=search(T->lchild,x)) return p;//在左子树中查找
return search(T->rchild,x);//在右子树中查找
}
}
二叉链表空间效率这么低,可以用它来存放当前结点遍历序列的直接前驱和后继等线索,以加快查找速度。
树和森林
树的表示法
双亲表示法:利用每个非根结点有一个双亲的性质,在每个结点附设指示器指示其双亲所在位置。
特点:找双亲谷易,找孩子难
孩子表示法:
1.多重链表表示法
每个结点有多个指针域,其中每个指针指向一棵子树根结点。结点有两种方式:
①链表中结点同构,链表中域的数目为树的度
② 非固定大小结点结构
若结点采用格式①表示,则空间可能会较浪费;若结点采用格式②表示,则操作较为不便。
2.孩子链表表示法:把树的每个结点的孩子排列起来,看成一个线性表,且以单链表作存储结构。则n个结点的树就有n个孩子链表;并将n个头指针也看成一个线性表,采用顺序存储结构。
孩子链表便于涉及孩子的操作的实现,却不适用于涉及双亲的操作,可将其和双亲表示结合在一起。
3.二叉链表(孩子-兄弟)表示法:链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点。
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
树与二叉树的对应关系
森林与二叉树的转换
1.树转化为二叉树
以二叉链表为媒介可导出树与二叉树之间的一个对应关系。即给定一棵树,可以找到唯一的一棵二叉树与之对应。用一棵二叉树表示一棵树,可以很好地解决树的存储表示问题,而且树的各种操作均可对应二叉树的操作来完成。
转换方法:
连兄弟:在树中个兄弟之间加一连线
断父子:对于任一结点,只保留它与最左孩子之间的连线
转一转:将所有横平竖直的分支顺时针转45°
2.二叉树转化为树
转换方法:
连祖孙:将结点与其左孩子的右子孙连接
断父子:对于任一结点,只保留它与左孩子之间的连线
抖一抖:将结点按层次排列,形成树结构
3.森林转化为二叉树
转换方法:
将森林中的每一棵树依次转换成相应的二叉树;将第二棵作为第一棵二叉树的根结点的右子树连接起来,将第三棵又作为第二棵的右子树连接起来...…直至把所有的二叉树连接成一棵二叉树。
4.二叉树转化为森林
转换方法:
将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树。将森林中的每一棵二叉树依次转换成树。
树和森林的遍历
[树的遍历]
遍历方法:
先根(序)遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
后根(序)遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。
按层次遍历:若树不空,则自上而下自左至右访问树中每个结点。
先根序列:ABEFCDGHIJK
后根序列:EFBCIJKHGDA
层次序列:ABCDEFGHIJK
[森林的遍历]
森林由三部分构成:森林中第一棵树的根结点、森林中第一棵树的子树森林、森林中其它树构成的森林。
1.先序遍历
若森林不空,则访问森林中第一棵树的根结点。
先序遍历森林中第一棵树的子树森林。
先序遍历森林中(除第一棵树之外)其余树构成的森林。
即依次从左至右对森林中的每一棵树进行先根遍历。
2.中序遍历
若森林不空,则中序遍历森林中第一棵树的子树森林。
访问森林中第一棵树的根结点。
中序遍历森林中(除第一棵树之外)其余树构成的森林。
即依次从左至右对森林中的每一棵树进行后根遍历。
树与森林的遍历和二叉树的遍历的对应关系
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
哈夫曼树(最优二叉树)
结点的路径长度:从根到该结点的路径上分支数目。
树的路径长度:树中所有结点的路径长度之和;在结点数相同的条件下,完全二叉树是路径长度最短的二叉树。
哈夫曼树的特点:
1.权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点。
2.只有度为0(叶子结点)和度为2(分支结点)的结点,不存在度为1的结点.
结点的权:即结点的值,具体含义视情况而定。
结点的带权路径长度:从该节点到树根的路径长度与节点上权值的乘积。
树的带权路径长度:树中所有叶子结点的带权路径长度之和,记作
哈夫曼树:假设有n个权值(),构造有n个叶子结点的二叉树,每个叶子结点带权为
,则其中带权路径长度WPL最小的二叉树称为哈夫曼树。
哈夫曼树的构造:
1.根据给定的n个权值,构造n棵只有一个根结点的二叉树,n个权值分别是这些树根结点的权。设森林F是由这n棵树构成的集合(原始森林)
2.在F中选取两棵根的权值最小的树作为左、右子树,构造一棵新二叉树,置新二叉树根的权值=左、右子树根结点权值之和(每次新增一个结点)
3.从F中删除这两棵树,并将新树加入F (每次少一棵树)
4.重复2、3,直到F中只含一颗树为止。(要重复n-1次)
例如:w={5,29,7,8,14,23,3,11},试以它们为叶子结点构造一棵Huffman树,求其带权路径长度。
八个权值从小到大排序是:3,5,7,8,11,14,23,29
图1:哈夫曼树
N100
/ \
N42 N58
/ \ / \
23 N19 29 N29
/ \ / \
11 N8 14 N15
/ \ / \
3 5 7 8
根结点N100到结点29的路径长度是2,结点29的带权路径长度是29*2
根结点N100到结点3的路径长度是4,结点3的带权路径长度是3*4
如此类推,哈夫曼树的带权路径长度
WPL=29*2+23*2+14*3+11*3+8*4+7*4+5*4+3*4=271
哈夫曼编码
利用二叉树来设计二进制的前缀码
如下图:a:0 b:10 c:110 d:111
设计总电文最短的前缀码:
假定以每种字符的出现次数或出现频率为w,编码长度为l,电文中共有n种字符,则电文编码总长度为:
即求以n种字符的频率为权,设计一棵Huffman树的问题,对应的编码称为Huffman编码。
例如:原文为WINNIE WILL WIN
W I N E L 3 4 3 1 2 1.构造以w(3)、l(4)、N(3)、E(1)、L(2)为叶子结点的最优二叉树
2.将该二叉树所有左分枝标记0,所有右分枝标记1
3.根结点到叶子结点所经过的二进制序列为该叶子结点字符的编码
W I N E L 00 010 011 10 11 发送方:00|11|10|10|11|010|00|11|011|011|00|11|10
译码:从Huffman树根开始,从待译码电文中逐位取码。若编码是“0”,则向左走;若编码是“1”,则向右走,一旦到达叶子结点,则译出一个字符;再重新从根出发,直到电文结束。
接收方:WINNIE WILL WIN
哈夫曼算法:
typedef struct{
int weght;
int parent,lchild,rchild;
}HTNode, *HuffmanTree;
void Select(HuffmanTree HT,int len,int &s1,int &s2)
{
int i,min1=32767,min2=32767;
for(i=1;i<=len;i++)
{
if(HT[i].weight<min1&&HT[i].parent==0)
{
s2=s1;
min2=min1;
min1=HT[i].weight;
s1=i;
}
else if(HT[i].weight<min2&&HT[i].parent==0)
{ min2=HT[i].weight;
s2=i;
}
}
}
void CreateHuffmanTree (HuffmanTree & HT,int n)//构造哈夫曼树HT
{
if (n<=1) return;
int m=2*n-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
for(i=1;i<=n;i++) scanf(HT[i].weight);
for(i=1;i<=m;++i)
{
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
//初始化工作结束,下面开始创建哈夫曼树
for(i=n+1;i<=m;++i)
{
Select(HT,i-1,s1,s2);
//在HT[1..i-1]选择parent为0且weight
//最小的两个结点,其序号分别为s1和s2
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+ HT[s2].weight;
}
}
typedef char **HuffmanCode;
void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
//从叶子到根逆向求每个字符的哈夫曼编码
{
char *cd=(char*)malloc(n*sizeof(char));
cd[n-1]='\0';
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
for(i=1;i<=n;++i)
{
int start=n-1;
int c=i;
int f=HT[i].parent;
while(f!=0)
{
--start;
if(HT[f].lchild==c) cd[start]='0';
else cd[start]='1';
c=f;
f=HT[f].parent;
}
HC[i]=(char*)malloc((n-start)*sizeof(char))
strcpy(HC[i],&cd[start]);
}
free(cd);
}