6.1 树的定义和基本术语
一. 树的抽象数据类型定义
- 树的定义
树是由n (n >=0)个结点组成的有限集合。
如果n = 0,称为空树;
如果n > 0,则:
有且只有一个特定的称之为根(root)的结点,它只有后继,但没有前驱;
除根以外的其它结点划分为m (m > 0)个互不相交的有限集合T1, T2, …, Tm,每个集合本身又是一棵树,并且称之为根的子树(SubTree)。每棵子树的根结点有且仅有一个直接前驱,但可以有0个或多个后继。
2. 树的表示方法
二. 树的基本术语
结点:数据元素+若干指向子树的分支
结点的度:结点拥有的分支个数
树的度:树中所有结点的度的最大值
叶子结点:度为零的结点
分支结点:度大于零的结点
(从根到结点的)路径:由从根到该结点所经分支和结点构成。
孩子结点、双亲结点、兄弟结点、堂兄弟
祖先结点、子孙结点
结点的层次:假设根结点的层次为1,第l 层的结点的子树根结点的层次为l+1
树的深度:树中叶子结点所在的最大层次
有序树:是指树中结点的各子树从左至右是有次序的(不能互换),否则称为无序树。
森林:是m(m≥0)棵互不相交的树的集合。
6.2 二叉树
一. 二叉树的定义
一棵二叉树是结点的一个有限集合,该集合
(1)或者为空,
(2)或者是由一个根结点组成
(3)或者是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。
二. 二叉树的操作
(1)InitBiTree(&T) (2)DestroyBiTree(&T)
(3)CreateBiTree(&T,definition)
(4)ClearBiTree(&T) (5)BiTreeEmpty(T)
(6)BiTreeDepth(T) (7)Root(t)
(8)Value(T,e) (9)Assign(T,&e,value)
(10)Parent(T,e) (11)LeftChild(T,e)
(12)RightChild(T,e) (13)InsertChild(T,p,LR,c) (14)DeleteChild(T,p,LR)
(15)PreOrderTraverse(T,Visit())
(16)InOrderTraverse(T,Visit()) (17)PostOrderTraverse(T,Visit())
……
三. 二叉树的性质
性质1 若二叉树的层次从1开始, 则在二叉树的第 i 层最多有 2^(i-1)个结点。(i >=1)
性质2 深度为k的二叉树最多有 2^k-1个结点。
(k >=1)
性质3 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2, 则有
n0=n2+1
三. 二叉树的性质
定义1 满二叉树(Full Binary Tree)
一棵深度为k且有2^k -1个结点的二叉树称为满二叉树。
定义2 完全二叉树(Complete Binary Tree)
深度为k,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。
或者:若设二叉树的深度为h,则共有h层。除第h层外,其它各层(0h-1)的结点数都达到最大个数,第h层从右向左连续缺若干结点。
完全二叉树的特点是:
1)只允许最后一层有空缺结点且空缺在右边,即叶子结点只能在层次最大的两层上出现;
2)对任一结点,如果其右子树的深度为l,则其左子树的深度必为l或l+1。
性质4 具有n个结点的完全二叉树的深度为 log2n +1
性质5 如果将一棵有n个结点的完全二叉树的结点按层序(自顶向下,同一层自左向右)连续编号1, 2, …, n,然后按此结点编号将树中各结点顺序地存放于一个一维数组中, 并简称编号为i的结点为结点i (1 <=i <=n)。则有以下关系:
若i = 1, 则 i 是二叉树的根,无双亲;
若i > 1, 则 i 的双亲为i /2
若2i > n, 则 i无左孩子;否则其左孩子结点为2i,
若2i+1 > n, 则 i无右孩子,否则其右孩子结点为2i+1,
二叉树的存储结构
顺序存储
链式存储
- 顺序存储结构(数组表示)
顺序存储二叉树的具体方法是:在一棵具有n个结点的完全二叉树中,从根结点开始编号为1,自上到下,每层自左至右地给所有结点编号,这样可以得到一个反映整个二叉树结构的线性序列;然后将完全二叉树上编号为i的结点依次存储在一维数组中下标为i-1的元素中。
#define MAX_TREE_SIZE 100
typedef TElemType SqBiTree[MAX_TREE_SIZE];
SqBiTree bt;
由于一般二叉树必须仿照完全二叉树那样存储,可能会浪费很多存储空间,单支树就是一个极端情况。
一棵深度为k且只有k个结点的单支树需要长度为2^K-1的一维数组 - 链式存储结构
链式存储是使用链表来存储二叉树中的数据元素,链表中的一个结点相应地存储二叉树中的一个结点。
常见的二叉树的链式存储结构有两种:二叉链表和三叉链表。
二叉链表是指链表中的每个结点包含两个指针域和一个数据域,分别用来存储指向二叉树中结点的左右孩子的指针及结点信息。
三叉链表是指链表中的每个结点包含三个指针域和一个数据域,相比二叉链表多出的一个指针域则用来指向该结点的双亲结点。
二叉链表存储表示
typedef struct BiTNode{
TElemType data;
Struct BiTNode *lchild,*rchild;
}BiTNode, *BiTree;
6.3 遍历二叉树和线索二叉树
“遍历”是任何类型均有的操作,对线性结构而言,只有一条搜索路径(因为每个结点均只有一个后继),故不需要另加讨论。而二叉树是非线性结构,每个结点有两个后继,则存在如何遍历即按什么样的搜索路径遍历的问题。
一、遍历二叉树
遍历:顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次。即找一个完整而有规律的走法,得到树中所有结点的一个线性排列。
遍历的结果:产生一个关于结点的线性序列。
设访问根结点记作 D,
遍历根的左子树记作 L
遍历根的右子树记作 R
则可能的遍历次序有:
先序 DLR, 逆先序DRL
中序 LDR, 逆中序RDL
后序 LRD, 逆后序RLD
先序遍历 (Preorder Traversal)
先序遍历二叉树算法的框架是
若二叉树为空,则空操作;
否则
访问根结点 (D);
先序遍历左子树 (L);
先序遍历右子树 ®。
中序遍历 (Inorder Traversal)
中序遍历二叉树算法的框架是
若二叉树为空,则空操作;
否则
中序遍历左子树 (L);
访问根结点 (D);
中序遍历右子树 ®。
后序遍历 (Postorder Traversal)
后序遍历二叉树算法的框架是
若二叉树为空,则空操作;
否则
后序遍历左子树 (L);
后序遍历右子树 ®;
访问根结点 (D)。
由二叉树的先序序列和中序序列可唯一地确定一棵二叉树。
遍历算法的递归描述
void PreOrder (BiTree T,
void( *Visit)(TElemType e))
{ // 先序遍历二叉树
if (T) {
Visit(T->data) ; // 访问结点
PreOrder(T->lchild, Visit); // 遍历左子树
PreOrder(T->rchild, Visit);// 遍历右子树
}
}
遍历二叉树(非递归算法)
中序遍历
在遍历左子树之前,先把根结点入栈,当左子树遍历结束后,从栈中弹出、访问,再遍历右子树
遍历的第一个和最后一个结点
第一个结点:沿着左链走,找到一个没有左孩子的结点;
最后一个结点:从根结点出发,沿着右链走,找到一个没有右孩子的结点;
void inorder(BiTree T){
InitStack(S);
BiTree p=T;
while(p||!StackEmpty(S){
if(p)
{Push(S,p);
p=p->lchild;}
else
{Pop(S,p);
printf("%c",p->data);
p=p->rchild;}
}
}
中序遍历的非递归算法
Status InOrderTraverse(BiTree T, Status (*Visit)(TElemType e))
{
InitStack(S); //根指针进栈
Push(S, T);
while (!StackEmpty(S)) {
while (GetTop(S, p) && p) Push(S, p->lchild); //向左走到尽头
Pop(S, p); //空指针退栈
if (!StackEmpty(S)) { //访问结点,向右一步
Pop(S, p);
if (!Visit(p->data)) return ERROR;
Push(S, p->rchild);
}
}
return OK;
}
先序遍历序列的形式定义一棵二叉树
根 左子树 右子树
先序建立二叉树的递归算法
Status CreateBiTree(BiTree &T)
{ char ch; scanf("%c",&ch);
if (ch==’ ') T=NULL;
else { if (!(T=(BiTNode*)malloc(sizeof(BiTNode))))
exit(OVERFLOW);
T->data = ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
return OK;
}
二、线索二叉树(穿线树、线索树) (Threaded Binary Tree)
线索:指向该线性序列中的“前驱”和“后继” 的指针。
线索链表:包含 “线索” 的存储结构。
线索二叉树:加上线索的二叉树
线索二叉树,即在一般二叉树的基础上,对每个结点进行考察。若其左子树非空,则其左指针不变,仍指向左子树;若其左子树为空,则让其左指针指向某种遍历顺序下该结点的前驱;若其右子树非空,则其右指针不变,仍指向右子树;若其右子树为空,则让其右指针指向某种遍历顺序下该结点的后继。如果规定遍历顺序为前序,则称为前序线索二叉树;如果规定遍历顺序为中序,则称为中序线索二叉树;如果规定遍历顺序为后序,则称为后序线索二叉树。
线索二叉树的类型描述
typedef enum PointerTag {Link,Thread};
//Link==0:指针,指向孩子结点
//Thread==1:线索,指向前驱或后继结点
typedef struct BiThrNode
{ TElemType data;
struct BiThrNode *lchild,*rchild;
PointerTag LTag,RTag;
}BiThrNode, *BiThrTree;
BiThrTree T;
1).在先序线索二叉树找前驱和后继
找后继:如果是非叶子结点,则:
如果有左孩子,则左孩子是其后继;
如果无左孩子,则右孩子是其后继;
如果是叶子结点,则右线索所指结点为后继;
找前驱:如果无左孩子,则左线索所指结点为前驱;
否则需要遍历
2).在中序线索二叉树找前驱和后继
找后继:若无右孩子,则右线索所指结点为后继;
否则,右孩子的“最左下”孩子为后继
找前驱:若无左孩子,则左线索所指结点为前驱;
否则,左孩子的“最右下”孩子为前驱
3).在后序线索二叉树找前驱和后继
找后继:如果无右孩子,则右线索所指结点为后继;
否则需要遍历;
找前驱:如果有右孩子,则右孩子是其前驱;
如果无右孩子,但有左孩子,则左孩子为前驱;
如果无左孩子,则左线索所指结点为前驱;
4). 线索二叉树上找前驱和后继的一般规律
先序线索二叉树找后继方便,找前驱可能需要遍历。
中序线索二叉树找前驱和后继都方便
后序线索二叉树找前驱方便,找后继可能需要遍历。
① 结束的条件?
树空或者指针指向头结点
② 中序遍历的第一个结点 ?
左子树上处于“最左下”(没有左子树)的结点
③ 在中序线索二叉树中结点的后继 ?
若无右子树,则右线索所指结点为后继
否则,右子树的“最左下”孩子为后继;
6.4 树和森林
树的三种存储结构
1.双亲表示法
用结构数组——树的顺序存储方式
类型定义:
找双亲方便,找孩子难
2.孩子链表表示法
顺序和链式结合的表示方法
找孩子方便,找双亲难
3.孩子兄弟表示法
找孩子容易,若增加parent域,则找双亲也较方便。
C语言的类型描述:
#define MAX_TREE_SIZE 100
结点结构:
ypedef struct PTNode {
TElemType data;
int parent; // 双亲位置域
} PTNode;
树结构:
typedef struct {
PTNode nodes [MAX_TREE_SIZE];
int r, n; // 根结点的位置和结点个数
} PTree;
树:typedef struct
{ CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根结点的位置
} CTree;
孩子链表头结点: typedef struct {
TElemType data;
ChildPtr firstchild; // 孩子链的头指针
} CTBox;
孩子链表结点:typedef struct CTNode {
int child;
struct CTNode *next;
} *ChildPtr;
森林与二叉树的转换
树转换为二叉树的步骤:
加线:在兄弟结点之间加一连线;
抹线:对任何结点,除了其最左的孩子
外,抹掉该结点原先与其孩子之
间的连线;
旋转:将水平的连线和原有的连线,以
树根结点为轴心,按顺时针方向
粗略地旋转450。
二叉树还原成树
(二叉树还原成树的步骤)
加线:如果p结点是双亲结点的左孩子;
则将p结点的右孩子,右孩子的右
孩子,……,沿着右分支搜索到的
所有右孩子都分别与p结点的双
亲用线连接起来;
抹线:抹掉原二叉树中所有双亲结点与
右孩子的连线;
调整: 将结点按层次排列,形成树的结构.
森林转换成二叉树
森林转换成二叉树的步骤:
转换:将森林中的每一颗树转换成二
叉树;
连线:将各棵转换后的二叉树的根结
点相连;
旋转:将添加的水平线和原有的连线,
以第一颗树的根结点为轴心,按
顺时针方向旋转450。
森林转化成二叉树的规则
若森林F为空,即n = 0,则
对应的二叉树B为空二叉树。
若F不空,则
对应二叉树B的根root (B)是F中第一棵树T1的根root (T1);
其左子树为B (T11, T12, …, T1m),其中,T11, T12, …, T1m是root (T1)的子树;
其右子树为B (T2, T3, …, Tn),其中,T2, T3, …, Tn是除T1外其它树构成的森林。
6.5赫夫曼树及其应用
1、最优树的定义
路径:从树中一个结点到另一个结点之间的分支所构成的通路 。
路径长度:路径上分支的数目。
树的路径长度:树的根到每个结点的路径长度之和。
结点的带权路径长度:从树根到该结点之间的长度与结点权值的乘积 ( l * w )
树的带权路径长度:树中所有叶子结点的带权路径长度之和
在所有含 n 个叶子结点、并带相同权值的 m 叉树中,必存在一棵其带权路径长度取最小值的树,称为“最优树”。
赫夫曼树带权路径长度达到最小的二叉树即为赫夫曼树(最优二叉树)。
在赫夫曼树中,权值大的结点离根最近。
赫夫曼算法
——如何构造一棵赫夫曼树
(1) 由给定的n个权值{w0, w1, w2, …, wn-1},构造具有n棵二叉树的森林F = {T0, T1, T2, …, Tn-1},其中每一棵二叉树Ti只有一个带有权值wi的根结点,其左、右子树均为空。
(2) 重复以下步骤, 直到F中仅剩下一棵树为止:
① 在F中选取两棵根结点的权值最小的二叉树, 做为左、右子树构造一棵新的二叉树。置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
② 在F中删去这两棵二叉树。
③ 把新的二叉树加入F。
3、最优树的应用
假设电文由A,B,C,D四个字符组成.
它们的编码分别为00,01,10和11.则电文‘ABACCDA’ 的编码00010010101100, 总长为14位.
为减少编码长度,重新设 A,B,C,D四个字符的编码为0,00,1和01.则电文编码为000011010,总长为9位.
0000 ABA AAAA BB BAA
前缀编码指的是,任何一个字符的编码都不是同一字符集中另一个字符的编码的前缀。
利用赫夫曼树可以构造一种不等长的二进制编码,并且构造所得的赫夫曼编码是一种最优前缀编码,使所传电文的总长度最短。
数据文件压缩:
已知一个由A,B,C,D,E,F,G,H等八个字符组成的文件包含100个字符,如果对这八个字符都按等长编码:
000,001,010,011,100,101,110,111
则文件包含的总位数为:
100×3=300 位
现已知八个字符在文件中出现的个数分别为:
5,29,7,8,14,23,3,11
如果采用赫夫曼编码:
0110,10,1110,1111,110,00,0111,010
则文件的总位数为:
5×4+29×2+7×4+8×4+14×3+23×2+11×3
=259 位
压缩率为13%
建立赫夫曼树及求赫夫曼编码的算法
typedef struct
{
unsigned int weight;
unsigned int parent,lchild,rchild;
} HTNode, *HuffmanTree;
typedef char **HuffmanCode;
void HuffmanCoding
(HuffmanTree &HT,HuffmanCode &HC,int *w, int n)
{ HuffmanTree p; char *cd; int i,s1,s2,start;
unsigned int c,f;
if (n<=1) return; // n为字符数目,m为结点数目
int m=2*n-1;
HT = (HuffmanTree)malloc((m+1)*sizeof(HTNode));
// 0号单元未用
for (p=HT, i=1; i<=n; ++i,++p,++w)
{ p->weight = *w; p->parent=0;
p->lchild=0;p->rchild=0; } // *p = { *w,0,0,0 };
for (; i<=m;++i,++p)
{ p->weight = 0; p->parent=0;
p->lchild=0; p->rchild=0; } //*p={ 0,0,0,0 };
for (i=n+1; i<=m;++i) // 建赫夫曼树
{ Select(HT,i-1,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;
}
//从叶子到根逆向求赫夫曼编码
HC= (HuffmanCode)malloc((n+1)*sizeof(char *));
cd = (char*)malloc(n*sizeof(char));
cd[n-1]='\0';
for (i=1;i<=n;++i)
{ start = n-1;
for (c=i,f=HT[c].parent; f!=0; c=f,f=HT[f].parent)
if (HT[f].lchild ==c) cd[--start]='0';
else cd[--start]='1';
HC[i]=(char *)malloc((n-start)*sizeof(char));
strcpy(HC[i],&cd[start]);
printf("%s\n",HC[i]);
}
free(cd);
}