目录
二叉树
可以证明,所有树都能转为唯一对应的二叉树,不失一般性。二叉树与树可以相互转换。
1 二叉树的抽象数据类型形式定义
ADT BinaryTree{
数据对象D:D是具有相同性质的数据元素的集合。
数据关系R:
若 D=ɸ,则R=ɸ,称BinaryTree为空二叉树;
若D!=ɸ,则R={H},H是如下二元关系:
(1) 在D中存在唯一的称为根的数据元素root,它在关系H下无前驱;
(2) 若D-{root}!=ɸ,则存在D-{root}={Dl,Dr},且DlՈDr=ɸ;
(3) 若 Dl!= ɸ,则Dl中存在唯一的元素xl,<root,xl> ε H,且存在Dl上的关系Hl ε H;若Dr!= ɸ,则Dr中存在唯一的元素xr,<root,xr> ε H,且存在Dr上的关系Hr ε H;H={<root,xl>,<root,xr>,Hl,Hr};
(4) (Dl,{Hl}) 是一颗符合本定义的二叉树,称为左子树,(Dr,{Hr}) 是一颗符合本定义的二叉树,称为右子树。
基本操作P:
PreOrderTraverse( T )
初始条件: 二叉树T存在。
操作结果:先序遍历T。
InOrderTraverse( T )
初始条件: 二叉树T存在。
操作结果:中序遍历T。
PostOrderTraverse( T )
初始条件: 二叉树T存在。
操作结果:后序遍历T。
LevelOrderTraverse( T )
初始条件: 二叉树T存在。
操作结果:层次遍历T。
CreateBiTree( &T, definition )
初始条件: definition 给出二叉树T的定义。
操作结果:按 definition构造二叉树T。
CopyBiTree( T, &NewT )
初始条件: 二叉树T存在。
操作结果:复制二叉树T到NewT。
BiTreeDepth( T )
初始条件: 二叉树T存在。
操作结果: 返回T的深度。
NodeCount( T )
初始条件: 二叉树T存在。
操作结果: 返回T的结点数。
LeafCount( T )
初始条件: 二叉树T存在。
操作结果: 返回T的叶子结点数。
}ADT BinaryTree
2 二叉树的顺序存储结构
#define MAXTSIZE 100 //二叉树的最大结点数
typedef TElemType SqBiTree[ MAXTSIZE ]; //零号单元存储根结点
SqBiTree bt;
这种顺序存储结构仅适用于完全二叉树。因为,在最坏的情况下,一个深度为K且只有K个结点的单支数(数中不存在度为2的结点)却需要长度为2k-1的一维数组。
3 二叉树的链式存储结构
(1)二叉树的二叉链表存储表示
typedef struct BiTNode{
TElemType data; //数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
(2)二叉树的三叉链表存储表示
typedef struct TriTNode{
TElemType data; //数据域
struct BiTNode *lchild,*parent,*rchild; //左右孩子指针和指向双亲指针
}TriTNode,*TriTree;
在具体应用中采用什么存储结构,除根据二叉树的形态之外还应考虑需进行何种操作。
4 二叉树的基本操作
以下操作,均为二叉链表的二叉树实现操作。
Status
PreOrderTraverse( BiTree T ){
// 二叉树先序遍历
if( T==NULL ) //空二叉树
return OK;
else{
visit( T ); //访问根结点
PreOrderTraverse( T->lchild ); //递归遍历左子树
PreOrderTraverse( T->rchild ); //递归遍历右子树
}
}
Status
InOrderTraverse( BiTree T ){
// 二叉树中序遍历
if( T==NULL ) //空二叉树
return OK;
else{
PreOrderTraverse( T->lchild ); //递归遍历左子树
visit( T ); //访问根结点
PreOrderTraverse( T->rchild ); //递归遍历右子树
}
}
Status
PostOrderTraverse( BiTree T ){
// 二叉树后序遍历
if( T==NULL ) //空二叉树
return OK;
else{
PreOrderTraverse( T->lchild ); //递归遍历左子树
PreOrderTraverse( T->rchild ); //递归遍历右子树
visit( T ); //访问根结点
}
}
void
LevelOrderTraverse( BTNode *b ){
BTNode *p; SqQueue *qu;
InitQueue( qu ); //初始化顺序循环队列
EnQueue( qu, b ); //根结点指针进入队列
while( !QueueEmpty( qu ) ){ //队不为空,则循环
DeQueue( qu, p ); //出队结点p
printf("%c",p->data); //访问结点p
if( p->lchild != NULL )
EnQueue( qu, b->lchild ); //有左孩子时将其进队
if( p->rchild != NULL )
EnQueue( qu, b->rchild ); //有右孩子时将其进队
}
}
Status
CreateBiTree( BiTree T ){
//按先序次序输入二叉树中结点的值,#代表空数
//构造二叉链表表示二叉树T
scanf( &ch ); //cin>>ch;
if( ch == '#')
T = NULL;
else{
if( !( T = ( BiTNod * )malloc( sizeof( BiTNod ))))
exit(OVERFLOW); //T = new BiTNode
T->data = ch; //生成根结点
CreateBiTree( T->lchild ); //构造左子树
CreateBiTree( T->rchild ); //构造右子树
}
return OK;
}
Status
CopyBiTree( BiTree T, BiTree &NewT ){
if( T=NULL ){ //如果是空树返回0
NewT = NULL;
return 0;
}else{
NewT = new BiTNode; //申请新结点
NewT->data = T->data; //复制根结点
CopyBiTree( T->lchild, NewT->lchild ); //递归复制左子树
CopyBiTree( T->rchild, NewT->rchild ); //递归复制右子树
}
}
Status
BiTreeDepth( BiTree T ){
if( T=NULL ) //如果是空树返回0
return 0;
else{
m = BiTreeDepth( T->lchild ); //递归计算左子树深度
n = BiTreeDepth( T->rchild ); //递归计算右子树深度
if( m > n ) return ( m + 1); //二叉树深度则为m与n的较大者+1
else return ( n + 1);
}
}
Status
NodeCount( BiTree T ){
if( T=NULL ) //如果是空树返回0
return 0;
else
return NodeCount( T->lchild ) + NodeCount( T->rchild ) + 1; //结点个数为左子树结点个数+右子树结点个数再+1
}
Status
LeafCount( BiTree T ){
if( T=NULL ) //如果是空树返回0
return 0;
if( T->lchild == NULL && T->rchild == NULL )
return 1; //如果是叶子结点返回1
else
return LeafCount( T->lchild ) + LeafCount( T->rchild ); //叶子结点数为左子树叶子结点数+右子树的叶子结点数
}
树
1 树的存储结构
(1)双亲表示法
假设以一组连续空间存储树的结点,同时在每个结点中附设一个指示器指示其双亲结点在链表中的位置。
#define MAXTSIZE 100
typedef struct PTNode{ //结点结构
TElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct { //树结构
PTNode nodes[ MAXTSIZE ];
int r,n; //根的位置和结点数
}PTree;
(2)孩子表示法
把每个结点的孩子结点排列起来,看成是一个线性表,且以单链表做存储结构,则n个结点有n个孩子链表,而n个头指针又组成一个线性表。为了便于查找,可采用顺序存储结构。
typedef struct CTNode{ //孩子结点
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct{
TElemType data;
ChildPtr firstchild; //孩子链表头指针
}CTBox;
typedef struct{
CTBox nodes[ MAXTSIZE ];
int n,r; //结点数和根的位置
}CTree;
(3)孩子兄弟表示法
又称二叉树表示法。即以二叉链表作为树的存储结构。链表中的结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点。
#typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
2 树和二叉树的转换
树转换成二叉树:兄弟相连留长子
二叉树转换成树:左孩右右连双亲,去掉原来右孩线
森林转换成二叉树:树变二叉根相连
二叉树转换成森林: 去掉全部右孩线,孤立二叉再还原
3 树和森林的遍历
由树结构的定义可引出两种次序的遍历树的方法:一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每颗子树;另一种是后跟遍历树,即先依次后跟遍历每颗子树,然后访问根结点。
按照森林和树的递归定义,我们可以推出森林的两种遍历方法:一种是先序遍历森林,即先访问森林中第一颗树的根结点,先序遍历第一颗树中根结点的子树森林,先序遍历除去第一颗树之后剩余的数树构成的森林;第二种是中序遍历森林,即中序遍历森林中第一颗树的根结点的子树森林,访问第一颗树的根结点,中序遍历除去第一颗树之后剩余的根结点的子树森林。
上述森林的先序和中序遍历即为相对应的二叉树的先序和中序遍历;树的先根遍历和后根遍历可借用二叉树的先序遍历和中序遍历的算法。
赫夫曼树
赫夫曼树,又称最优树,是一类带权路径长度最短的树。
路径长度:路径上的分支数目称做路径长度。
树的路径长度:从树根到每一个结点的路径长度之和。
树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作WPL。
假设有n个权值{w1,w2,…,wn},试构造一颗有n个叶子结点的二叉树,每个叶子结点带权为wi,则其中带权路径长度WPL最小的二叉树称做最优二叉树或赫夫曼树。赫夫曼树中权越大的叶子离根越近。
1 赫夫曼算法
(1) 根据给定的n个权值{w1,w2,…,wn}构成n颗二叉树的集合F={T1,T2,…,Tn},其中每颗二叉树Ti中只有一个带权为wi的根结点,其左右子树均空。(构造森林全是根)
(2) 在F中选取两颗根结点的权值最小的树作为左右子树构造一颗新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。(选用两小造新树)
(3) 在F中删除这两颗树,同时将新得到的二叉树加入F中。(删除两小添新人)
(4) 重复(2)(3),直到F只含一棵树为止。这颗树便是赫夫曼树。(重复2,3剩单根)
包含n棵树的森林要经过n-1次合并才能形成赫夫曼树,共产生n-1个新结点。赫夫曼树共有2n-1个结点,可以存储在一个大小为2n-1的一维数组中。
2 赫夫曼算法的实现
typedef struct{
int weight; //权值
int parent,lch,rch;
}HTNode,*HuffmanTree;
void
CreatHuffmanTree( HuffmanTree HT, int n ){ //构造赫夫曼树
if( n <= 1 ) retuen;
m = 2*n - 1; //数组共2n-1个元素
HT = new HTNode[ m + 1 ]; //0号单元未用,HT[m]表示根结点
for( i = 1; i <= m; ++i ){ //将2n-1个元素的lch,rch,parent置为0
HT[ i ].lch = 0;
HT[ i ].rch = 0;
HT[ i ].parent = 0;
}
for( i = 1; i <= m; ++i ) //输入前n个元素的weight值
cin >> HT[ i ].weight;
//初始化结束,下面开始建立赫夫曼树
for( i = n + 1; i <= m; i++ ){ //合并并产生n-1个结点,构造赫夫曼树
Select( HT, i-1, s1, s2 ); //在HT[k](1<=k<=i-1)中选择两个其双亲域为0且权值最小的结点,并返回他们在HT中的序号s1和s2
HT[ s1 ].parent = i;
HT[ s1 ].parent = i; //表示从F中删除s1和s2
HT[ i ].lch = s1;
HT[ i ].rch = s2; //s1和s2分别作为左右孩子
HT[ i ].weight = HT[ s1 ].weight + HT[ s2 ].weight;
}
}
3 赫夫曼编码
若要设计长短不一的编码,则必须是任一个字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码。
什么样的前缀码能使得电文总长最短?
void
CreatHuffmanCode( HuffmanTree HT, HuffmanCode &HC, int n ){
//从叶子到根逆向求每个字符的赫夫曼编码,存储在编码表HC中
HC = new char *[ n + 1 ]; // 分配n个字符编码的头指针矢量
cd = new char [n]; //分配临时存放编码的动态数组空间
cd[ n - 1 ] = '\0'; //编码结束符
for( i = 1; i <= n; ++i ){ //逐个字符求赫夫曼编码
start = n-1;
c = i;
f = HT[ i ].parent;
whlie( f != 0 ){ //从叶子结点向上回溯,直到根结点
--start; //回溯一次start向前指向一个位置
if( HT[ f ].lch == c )
cd[ start ] = '0'; //结点c是f的左孩子,则生成代码0
else
cd[ start ] = '1'; //结点c是f的右孩子,则生成代码1
c = f;
f = HT[ f ].parent; //继续向上回溯
}
HC[ i ] = new char [n - start]; //为第i个字符串编码分配空间
strcpy( HC[ i ], &cd[ start ] ); //将求得的编码从临时空间cd复制到HC的当前行列中
}
delete cd; //释放临时空间
}