目录
6.1 树的定义
树的定义
逻辑特点
基本术语
6.2 二叉树的定义
二叉树基本特点
树和二叉树的抽象数据类型定义
二叉树的顺序存储
二叉树的链式存储
6.3 遍历二叉树和线索二叉树
先序遍历
中序遍历
后序遍历
层序遍历
求高度
求结点总数
求叶子结点总数
先序输出叶子结点
计算给定二叉树T的宽度。二叉树的宽度是指各层结点数的最大值。
线索化二叉树
线索化二叉树的几个术语
6.4 树和森林
树的存储结构--二叉链表表示法
树、森林与二叉树之间的相互转换
1.树或森林转换为二叉树
2.二叉树转化为树或森林
6.5 哈夫曼树及其应用
构造过程
树(Tree)是n(n≥0)个结点的有限集,它或为空树(n = 0);或为非空树,对于非空树T:
(1)有且仅有一个称之为根的结点;
(2)除根结点以外的其余结点可分为m(m>0)个互不相交的有限集T1, T2, …, Tm, 其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
1.有且仅有一个结点没有前驱结点,该结点为树的根节点;
2.除了根节点外,每个节点有且仅有一个直接前驱结点;
3.包括根节点在内,每个结点可以有多个后继结点
根——即根结点(没有前驱)
叶子——即终端结点(没有后继)
森林——指m棵不相交的树的集合(例如删除A后的子树个数)
有序树——结点各子树从左至右有序,不能互换(左为第一)
无序树——结点各子树可互换位置。
双亲——即上层的那个结点(直接前驱)
孩子——即下层结点的子树的根(直接后继)
兄弟——同一双亲下的同层结点(孩子之间互称兄弟)
堂兄弟——即双亲位于同一层的结点(但并非同一双亲)
祖先——即从根到该结点所经分支的所有结点
子孙——即该结点下层子树中的任一结点
结点——即树的数据元素
结点的度——结点挂接的子树数
结点的层次——从根到该结点的层数(根结点算第一层)
终端结点——即度为0的结点,即叶子
分支结点——即度不为0的结点(也称为内部结点)
树的度——所有结点度中的最大值
树的深度(高度)——指所有结点中最大的层数
二叉树(Binary Tree)是n(n≥0)个结点所构成的集合,它或为空树(n = 0);或为非空树,对于非空树T:
(1)有且仅有一个称之为根的结点;
(2)除根结点以外的其余结点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。
•结点的度小于等于2
•有序树(子树有序,不能颠倒)
ADT BinaryTree{
数据对象D:D是具有相同特性的数据元素的集合。
数据关系R:
若D=Φ,则R= Φ ;
若D≠Φ,则R= {H};存在二元关系:
① root 唯一 //关于根的说明
② Dj∩Dk= Φ //关于子树不相交的说明
③ …… //关于数据元素的说明
④ …… //关于左子树和右子树的说明
基本操作 P:
CreateBiTree(&T,definition)
初始条件;definition给出二叉树T的定义。
操作结果:按definition构造二叉树T。
PreOrderTraverse(T)
初始条件:二叉树T存在。
操作结果:先序遍历T,对每个结点访问一次。
InOrderTraverse(T)
初始条件:二叉树T存在。
操作结果:中序遍历T,对每个结点访问一次。
PostOrderTraverse(T)
初始条件:二叉树T存在。
操作结果:后序遍历T,对每个结点访问一次。
}ADT BinaryTree
二叉树的性质和存储结构
性质1: 在二叉树的第i层上至多有2^(i-1)个结点
性质2: 深度为k的二叉树至多有2^k-1个结点
性质3: 对于任何一棵二叉树,若2度的结点数有n2个,则叶子数n0必定为n2+1 (即n0=n2+1)
满二叉树:一棵深度为k 且有2^k -1个结点的二叉树。(特点:每层都“充满”了结点)
完全二叉树:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满二叉树中编号从1至n的结点一一对应
性质4: 具有n个结点的完全二叉树的深度必为[log2n]+1
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2。
实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素。
特点:
结点间关系蕴含在其存储位置中
浪费空间,适于存满二叉树和完全二叉树
typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild; //左右孩子指针
}BiNode,*BiTree;
三叉链表
typedef struct TriTNode
{
TelemType data;
struct TriTNode *lchild,*parent,*rchild;
}TriTNode,*TriTree;
遍历定义——指按某条搜索路线遍访每个结点且不重复(又称周游)。
遍历用途——它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
口诀:
DLR—先序遍历,即先根再左再右
LDR—中序遍历,即先左再根再右
LRD—后序遍历,即先左再右再根
Status PreOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
cout<data; //访问根结点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild); //递归遍历右子树
}
}
Status InOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
InOrderTraverse(T->lchild); //递归遍历左子树
cout<data; //访问根结点
InOrderTraverse(T->rchild); //递归遍历右子树
}
}
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
PostOrderTraverse(T->lchild); //递归遍历左子树
PostOrderTraverse(T->rchild); //递归遍历右子树
cout<data; //访问根结点
}
}
如果去掉输出语句,从递归的角度看,三种算法是完全相同的,或说这三种算法的访问路径是相同的,只是访问结点的时机不同。
时间效率:O(n) //每个结点只访问一次
空间效率:O(n) //栈占用的最大辅助空间
void LevelorderTraversal( BinTree BT )
{
if(!BT)
return;
BinTree q[505];
int front = 0,rear = 0;
q[rear++] = BT;
while(front < rear)
{
printf(" %c",q[front]->Data);
if(q[front]->Left)
{
q[rear++] = q[front]->Left;
}
if(q[front]->Right)
{
q[rear++] = q[front]->Right;
}
front++;
}
}
int GetHeight( BinTree BT )
{
if(!BT)
return 0;
int lh = GetHeight(BT->Left);
int rh = GetHeight(BT->Right);
if(lh>rh)
return lh+1;
return rh+1;
}
int NodeCount(BiTree T)
{
if(T == NULL ) return 0;
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
int LeadCount(BiTree T){
if(T==NULL) //如果是空树返回0
return 0;
if (T->lchild == NULL && T->rchild == NULL)
return 1; //如果是叶子结点返回1
return LeafCount(T->lchild) + LeafCount(T->rchild);
}
void PreorderPrintLeaves( BinTree BT )
{
if(!BT)
return;
if(!BT->Left && !BT->Right)printf(" %c",BT->Data);
PreorderPrintLeaves(BT->Left);
PreorderPrintLeaves(BT->Right);
}
T
的宽度。二叉树的宽度是指各层结点数的最大值。typedef struct TreeNode *BinTree;
struct TreeNode
{
int Key;
BinTree Left;
BinTree Right;
};
int Width( BinTree T )
{
BinTree p;
Queue Q;
int Last, temp_width, max_width;
temp_width = max_width = 0;
Q = CreateQueue(MaxElements);
Last = Queue_rear(Q);
if (T == NULL) return 0;
else
{
Enqueue(T, Q);
while (!IsEmpty(Q))
{
p = Front_Dequeue(Q);
temp_width++;
if ( p->Left != NULL ) Enqueue(p->Left, Q);
if ( p->Right != NULL ) Enqueue(p->Right, Q);
if ( Queue_front(Q) > Last )
{
Last = Queue_rear(Q);
if ( temp_width > max_width ) max_width = temp_width;
temp_width = 0;
}
}
return max_width;
}
}
若二叉树中各结点的值均不相同,则:
由二叉树的前序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树,
但由前序序列和后序序列却不一定能唯一地确定一棵二叉树。
普通二叉树只能找到结点的左右孩子信息,而该结点的直接前驱和直接后继只能在遍历过程中获得
若将遍历后对应的有关前驱和后继预存起来,则从第一个结点开始就能很快“顺藤摸瓜”而遍历整个树
例如中序遍历结果:B D C E A F H G,实际上已将二叉树转为线性排列,显然具有唯一前驱和唯一后继!
1)若结点有左子树,则lchild指向其左孩子;
否则, lchild指向其直接前驱(即线索);
2)若结点有右子树,则rchild指向其右孩子;
否则, rchild指向其直接后继(即线索) 。
为了避免混淆,增加两个标志域
lchild |
LTag |
data |
RTag |
rchild |
LTag :若 LTag=0, lchild域指向左孩子;
若 LTag=1, lchild域指向其前驱。
RTag :若 RTag=0, rchild域指向右孩子;
若 RTag=1, rchild域指向其后继。
线索:指向结点前驱和后继的指针
线索链表:加上线索二叉链表
线索二叉树:加上线索的二叉树(图形式样)
线索化:对二叉树以某种次序遍历使其变为线索二叉树的过程
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
树或森林与二叉树之间存在一一对应的关系,相互之间可以进行转换
(1)凡是兄弟就用线连起来
(2)除第一个子女外的其他子女均去掉到父母的连线
设B是一棵二叉树,root是B的根,L是root的左子树,R是root的右子树。则对应于B的森林F(B)的定义为:
⑴ 若B为空,则对应的森林F(B)也为空;
⑵ 若B非空,则F(B)是第一棵树T1,加上森林F(R),其中树T1的根为root,root的子树为F(L)。
关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀-前缀编码
采用二叉树设计前缀编码 左分支用“0”,右分支用“1”
分解接收字符串:遇“0”向左,遇“1”向右;一旦到达叶子结点,则译出一个字符,反复由根出发,直到译码完成。
特点:每一码都不是另一码的前缀,绝不会错译! 称为前缀码
基本思想:使权大的结点靠近根
操作要点:对权值的合并、删除与替换,总是合并当前值最小的两个
根据给定的n个权值{w1,w2,……wn},构造n棵只有根结点的二叉树。
在森林中选取两棵根结点权值最小的树作左右子树,构造一棵新的二叉树,置新二叉树根结点权值为其左右子树根结点权值之和。
在森林中删除这两棵树,同时将新得到的二叉树加入森林中。
重复上述两步,直到只含一棵树为止,这棵树即哈夫曼树。
一棵有n个叶子结点的Huffman树有 2n - 1 个结点