树和二叉树
树是一类重要的非线性数据结构
树和二叉树
树的定义和基本术语
定义
树是n个结点的有限集T
其中:
- 有且仅有一个特点的结点称为树的根
- 当n>1时,其余的结点可分为m个互不相交的有限集T1,T2……Tm,其中每一个集合本身又是一棵树,称为根的子树
基本术语
- 结点——数据元素+若干指向子树的分支
- 结点的度——分支的个数,即结点拥有的子树数
- 树的度——树内各节点的度的最大值
- 叶子或终端结点——度为0的结点
- 分支结点或非终端结点——度大于零的结点
- 孩子——结点的子树的根称为该结点的孩子
- 双亲——孩子结点的上层结点称作该节点的双亲
- 兄弟——同一双亲的孩子
- 堂兄弟——其双亲在同一层的结点
- 祖先——从根到该节点所经分支上的所有结点
- 子孙——以某结点为根的子树中的任一结点都称为该结点的子孙
- 结点的层次——假设根结点的层次为 1,第l层的结点的子树根结点的层次为l+1
- 深度——树中结点的最大层次
- 有向树:
- 有确定的树根
- 树根和子树根之间为有向关系
- 有序树
- 子树之间存在确定的次序关系
- 无序树
- 子树之间不存在确定的次序关系
森林的简单定义
定义:
是m课互不相交的树的集合
任何一棵非空树是一个二元组:Tree = (root,F)
其中:
- root被称为根结点
- F被称为子树森林
线性结构和树型结构
树的基本操作(下面会具体存储结构进行实现)
**基本操作——查找**
Root(T) // 求树的根结点
Value(T,cur_e) //求当前结点的元素值
Parent(T,cur_e) //求当前结点的双亲结点
LeftChild(T, cur_e) // 求当前结点的最左孩子
RightSibling(T, cur_e) // 求当前结点的右兄弟
TreeEmpty(T) // 判定树是否为空树
TreeDepth(T) // 求树的深度
TraverseTree( T, Visit() ) // 遍历
**基本操作——插入**
InitTree(&T) // 初始化置空树
CreateTree(&T, definition) // 按定义构造树
Assign(T, cur_e, value) // 给当前结点赋值
InsertChild(&T, &p, i, c) // 将以c为根的树插 入为结点p的第i棵子树
**基本操作——删除**
ClearTree(&T) // 将树清空
DestroyTree(&T) // 销毁树的结构
DeleteChild(&T, &p, i) // 删除T中p所指结点 的第i棵子树
二叉树
定义
二叉树或为空树(n=0),或由一个根结点和两棵 分别称为左子树和右子树的互不相交的二叉树构成
特点:
- 每个结点至多有二棵子树(即不存在度大于2的 结点)
- 二叉树的子树有左、右之分,且其次序不能任 意颠倒
二叉树的五种基本形态
二叉树的性质
- 性质一:在二叉树的第i层至多有
个结点
- 性质二:深度为k的二叉树上至多含
个结点
- 性质三:对任何一棵二叉树,若它含有n0 个叶子结 点、n2 个度为2的结点,则必存在关系式: n0= n2+1。
- 性质四:具有n个结点的完全二叉树的深度为
- 若对含n 个结点的完全二叉树从上到下且从左 至右进行1至n的编号,则对完全二叉树中任意 一个编号为i的结点:
- 若i=1,则该结点是二叉树的根,无双亲, 否则,编号为[i/2] 的结点为其双亲结点
- 若2i>n,则该结点无左孩子, 否则,编号为2i 的结点为其左孩子结点
- 若2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点
两种特殊的二叉树
满二叉树:
指的是深度为k且含有个结点的二叉树
完全二叉树:
树 中所含的n 个结点 和满二叉树中编号 为1 至n 的结点一 一对应。
二叉树的存储结构
二叉树的顺序存储表示
实现:
按满二叉树的结点层次编号, 依次存放二叉树中的数据元素
类型定义:
#define MAX_TRUE_SIZE 100 //二叉树的最大节点数
typedef TElemType SqBiTree[MAX_TREE_SIZE]; //零号单元存储根节点
SqBiTree bt;
特点:
结点间关系蕴含在其存储位 置中;浪费空间,适于存满二叉树 和完全二叉树
二叉树的链式存储表示
二叉链表
结点结构:
类型定义:
typedef struct BiTNode {//结点结构
TElemType data;
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
特点:
在n个结点的二叉链表中有n+1个空指针域
三叉链表
结点结构:
类型定义:
typedef struct TriTNode{ // 结点结构
TElemType data;
structTriTNode *lchild, *rchild; // 左右孩子指针
struct TriTNode*parent; //双亲指针
}TriTNode, *TriTree;
遍历二叉树和线索二叉树
遍历二叉树
顺着某一条搜索路径寻访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次
遍历是任何类型都有的操作,对于线性结构来说只有一条搜索路径,因为每个节点都只有一个后继。而二叉树是非线性结构,每个结点有两个后继,则存在着如何遍历即按什么样的搜索路径遍历的问题
对于二叉树来说,可以有三条路径:
- 先上后下的按层次遍历
- 先左子树后右子树的遍历(重点掌握)
- 先右子树后左子树的遍历
先左后右的遍历算法
-
先根(先序)遍历
遍历过程:
若二叉树为空树,则空操作;
否则:
(1)访问根结点
(2)先序遍历左子树
(3)先序遍历右子树 -
中根(中序)遍历
遍历过程:
若二叉树为空树,则空操作;
否则:
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树。 -
后根(后序)遍历
遍历过程:
若二叉树为空树,则空操作;
否则:
(1)后序遍历左子树
(2)后序遍历右子树
(3)访问根结点。
算法描述
先序遍历的递归算法:
void Preorder(BiTree T,void(*visit)(TElemType& e)) {
//先序遍历二叉树
if(T) {
visit(T->data); //访问根节点
Preorder(T->lchild,visit); //遍历左子树
Preorder(T->rchild, visit);// 遍历右子树
}
}
上面算法的通用形式:
Status Preorder(BiTree T,Status(*visit)(TElemType &e)) {//先序遍历二叉树
if(T) {
if(visit(T->data))
if(Preorder(T->lchild,visit))
if(Preorder(T->data,visit))
return OK;
return ERROR;
}
else
return OK;
}
//这里的(*visit)是函数指针,即指向函数的指针,函数的地址 赋给指针。函数名表示函数的地址。
//最简单的访问函数:
Status PrintElement(TElemType e) {
print(e);
return OK;
}
//实际调用时:
Preorder(T,PrintElement)
最简单的访问直接输出结点:
void Preorder(BiTree T) {
//先序遍历二叉树
if(T) {
printf(“%d\t”,T->data);// 输出结点
Preorder(T->lchild); // 遍历左子树
Preorder(T->rchild);// 遍历右子树
}
}
中序遍历的递归算法:
void Inorder(BiTree T,void (*visit)(TElemType &e)) {
//中序遍历二叉树
if(T) {
Inorder(T->lchild,visit); //遍历左子树
visit(T->data); //访问根节点
Inorder(T->rchild,visit); //遍历右子树
}
}
后序遍历的递归算法:
void Postorder(BiTree T,void(*visit)(TElemType &e)) {//后序遍历二叉树
if(T) {
Postorder(T->lchild,visit); //遍历左子树
Postorder(T->rchild,visit); //遍历右子树
visit(T->data); //访问根节点
}
}
二叉树的中序遍历的非递归算法(两种实现方式):
Status InOrderTranverse(BiTree T,Status(*visit)(TElemType e)) {
Initstack(S);
Push(S,T); //根指针进栈
if(!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);
}//if
}//while
return OK;
}
Status InOrderTranverse(BiTree T,Status(*visit)(TElemType e)) {
InitStack(S); p=T;
while(p || !StackEmpty(S)) {
if(p) {
Push(S,p);
p = p->lchild;
//根指针进栈 遍历左子树
}
else { //根指针退栈 访问根节点 遍历右子树
Pop(S,p);
if(!visit(o->data))
return ERROR;
p = p->rchild;
}//else
}//while
return OK;
}
二叉树先序遍历的非递归算法:
Status preorder(BiTree T,Status(*visit)(TElemType e)) {
InitStack(S);
p = T;
while(p || !StackEmpty(S)) {
if(p) {
if(!visit(p->data))
return ERROR;
Push(S,p);
p = p->lchild;
}
else {
Pop(S,p);
p = p->rchild;
}
}
return OK;
}
二叉树后序遍历的非递归算法:
Status PostOrderTraverse1(BiTree T,Status(*visit)(TElemType e)) {
InitStack(S);p = T;
BiTree LastVisit = NULL;
while(p || !StackEmpty(S)) {
Push(S,p);
p = p->lchild;
}
GetTop(S,p);
if(!p->rchild || LastVisit == p->rchild) {
if(!visit(p->data))
return ERROR;
Pop(S,LastVisit); //LastVisit记录最后访问的结点
p = NULL; //将p指向NULL则下次进入while循环时 不做左子树进栈操作
}
else
p = p->rchild; //进到右子树
return OK;
}
遍历算法的应用
- 统计二叉树中叶子结点的个数(先序遍历)
算法的基本思想:先序遍历二叉树,在遍历的过程中查找叶子结点并计数(设置一个计数器)
void CountLeaf(BiTree T,int &count) {
if(T) {
if((!T->lchild) && (!T->rchild))
count++; //对叶子结点计数
CountLeaf(T->lchild,count);
CountLeaf(T->rchild,count);
}//if
}//CountLeaf
- 计算二叉树结点总数
算法的思想:- 如果是空树,则结点个数为0
- 否则,结点个数为左子树的结点个数+右子树的结点个数再加1
int NodeCount(BiTree T) {
if(T == NULL)
return 0;
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
- 求二叉树的深度(后序遍历)
算法的思想:首先分析二叉树的深度和它的左右子树深度之间的关系。
从二叉树的定义我们可得:二叉树的深度 = MAX(左右子树深度)+1.
int Depth(BiTree T) {//返回二叉树的深度
if(!T)
depthval = 0;
else {
depthLeft = Depth(T->lchild);
depthRight = Depth(T->rchild);
depthval = 1+(depthLeft>depthRight ? depthLeft:depthRight);
}
return depthcal;
}
- 复制二叉树(后序遍历)
//生成一个二叉树 的结点(其数据域是item 左指针为lptr 右指针域为rptr)
BiTNode *GetTreeNode(TElemType item,BiTNode *lptr,BiTree *rptr) {
if(!(T = (BiTNode*)malloc(sizeof(BiTNode))))
exit(1);
T->data = item;
T->lchild = lptr;
T->rchild = rptr;
return T;
}
BiTNode *CopyTree(BiTNode *T) {
if(!T)
return NULL;
if(T->lchild)
newlptr = CopyTree(T->lchild); //复制左子树
else
newlptr = NULL;
if(T->rchild)
newrptr = CopyTree(T->rchild); //复制右子树
else
newrptr = NULL;
newT = GetTreeNode(T->data,newlptr,newrptr);
return newT;
}
- 建立二叉树的存储结构
不同的定义方法相应有不同的存储结构的建立算法
Status CreateBiTree(BiTree &T) {
scanf(&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;
}
已知二叉树的先序和中序序列可以建树:
void CrtBT(BiTree &T,char pre[],char ino[],int ps,int is,int n) {
//已知pre[ps……ps+n-1]为二叉树的先序序列 ino[is……is+n-1]为二叉树的中序序列
if(n == 0)
T = NULL;
else {
k = Search(ino,pre[ps]); //在中序序列中查询
if(k == -1)
T = NULL;
else {
T = (BiTNode*)malloc(sizeof(BiTNode));
T->data = pre[ps];
if(k == is) T->lchild = NULL;
else CrtBT(T->lchild,pre[],ino[],ps+1,is,k-is);
if(l = is+n-1) T->rchild = NULL;
else CrtBT(T->rchild,pre[],ino[],ps+1+(k-is),k+1,n-(k-is)-1);
}
}
}
线索二叉树
什么是线索二叉树:
遍历二叉树的结果是求得结点的一个线性序列,两个相邻的结点互为前驱和后继。指向该线性序列中的前驱和后继的指针称作线索。
- 包含线索的存储结构称为线索链表
- 与其对应的二叉树称作线索二叉树
对线索链表中的结点的约定:
在二叉链表的结点中增加两个标志域,并做如下约定:
若该节点的左子树不空,则左标志域的值为0;
否则lchild域的指针指向其前驱且左标志的值为1
若该结点的右子树不空,则rchild的指针指向其右子树,且有标志域的值为0
否则,rchild域的指针指向其后继且有标志域值为1
实现:
- 在有n个结点的二叉链表中必定有n+1个空链域
- 在线索二叉树的结点中增加两个标志域
- LTag:若LTag=0,lchild域指向左孩子;若LTag=1,lchild域指向其前驱
- RTag:若RTag=0,rchild域指向右孩子;若RTag=1,rchild域指向其后继
这样定义的二叉链表作为二叉树的存储结构称为线索链表
线索化:对二叉树按某种遍历次序使其变成线索二叉树的过程
线索链表的类型描述:
typedef enum {Link,Thread} PointerTag;
//Link == 0; 指针 Thread == 1;线索
typedef struct BiThrNode {
TElemType data;
struct BiThrNode *lchild,*rchild; //左右指针
PointerTag LTag,RTag; //左右标志
}BiThrNode,*BiThrTree;
线索链表的遍历算法
由于在线索链表中添加了遍历中得到的前驱和后继的信息没从而简化了遍历的算法。
void InOrderYraverse_Thr(BiThrTree T) {
p = T->lchild; //p指向根节点
while(p != T) { //空树或遍历结束时 p == T
while(p->LTag == Link)
p = p->lchild; //第一个结点
visit(p->data);
while(p->RTag == Thread && p->rchild!=T) {
p = p->rchild;
visit(p->data);
}//访问后继结点
p = p->rchild; //p进至其右子树根
}
}
如何建立线索链表:
void InThreading(BiThrTree p) {
if(p) { //对以p为根的非空二叉树进行线索化
InThreading(p->lchild); //左子树线索化
if(!p->lchild) { //建前驱线索
p->LTag = Thread;
p->lchild = pre;
}
if(!p->rchild) {//建后继线索
pre->RTag = Thread;
pre->rchild = p;
}
pre = p; //保持pre指向p的前驱
InThreading(p->rchild); //右子树线索化
}
}
Status InorderThreading(BiThrTree &Thrt,BiThrTree T) {
Thrt = (BiThrTree)malloc(sizeof(BiThrTree));
if(!Thrt)
exit(OVERFLOW);
Thrt->LTag = Link;
Thrt->RTag = Thread; //建头结点
Thrt->rchild = Thrt; // 右指针回指
if(!T)
Thrt->lchild = Thrt; //若二叉树为空 则左指针回指
else {
Thrt->lchild = T; pre = Thrt;
InThreading(T);
pre->rchild = Thrt; //处理最后一个结点
pre->RTag = Thread;
Thrt->rchild = pre;
}
return OK;
}
树和森林
树的三种存储结构
双亲表示法
实现:
定义结构数组存放树的结点,,每个结点包含两个域:
- 数据域:存放结点的本身信息
- 双亲域:指示本结点的双亲结点在数组中位置
- 特点:找双亲容易,找孩子难
类型描述:
#define MAX_TRUE_SIZE 100
//结点结构
typedef struct PTNode {
TElemType data;
int parent; //双亲位置域
}PTNode;
//树结构
typedef struct {
PTNode nodes[MAX_TRUE_SIZE];
int r,n; //根节点的位置和结点个数
}PTree;
孩子表示法
多重链表:
每个结点有多个指针域,分别指向其子树的根
结点同构:
结点的指针个数相等,为树的度D
结点不同构:
结点指针个数不等,为该节点的度d
孩子链表:
每个结点的孩子结点用单链表存储,再用含n个元素的结构数组指向每个孩子链表
代码描述:
//结点定义为:孩子结点结构
typedef struct CTNode {
int child; //该结点在表头数组中下标
struct CTNode *next; //指向下一个孩子结点
}*ChildPtr;
//双亲结点结构:
typedef struct {
TElemType data; //数据域
ChildPtr firstchild; //指向第一个孩子结点
}CTBox;
//树结构
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n,r; //节点数和根的位置
}Ctree;
孩子-兄弟表示法
实现:
用二叉链表作为树的存储结构,链表中的每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
特点:
操作不容易,但是破坏了树的层次
//结点结构:
typedef struct CSNode {
ElemType data;
struct CSNode *firstchild.*nextsibling;
}CSNode,*CSTree;
森林和二叉树的转换
树与二叉树的转换
将树转换成二叉树的转换规则:
加线:在兄弟之间加一条线
抹线:对每个结点除了其左孩子外,去除与其余孩子之间的关系
旋转:以树的根节点为轴心将整树顺时针旋转45度
将二叉树转换成树的转换规则:
加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子……沿分支找到的所有的右孩子,都与p的双亲用线连起来
抹线:抹掉原二叉树中双亲与右孩子之间的连线
调整:将结点按层次排列,形成树结构
森林与二叉树的转换
森林转换成二叉树转换规则:
- 将各棵树分别转换成二叉树
- 将每棵树的根节点用线相连
- 以第一棵树根节点为二叉树的根,再以根节点为轴心,顺时针旋转,构成二叉树树形结构
二叉树转换成森林的转换规则:
- 抹线:将二叉树中根节点与其右孩子的连线及沿右分支搜索到的所有右孩子间的连线全部抹掉,使之变成孤立的二叉树
- huanyuan:将孤立 的二叉树还原成树
树和森林的遍历
树的遍历
按一定规律走遍树的各个顶点,且使每一 顶点仅被访问一次,即找一个完整而有规 律的走法,以得到树中所有结点的一个线 性排列。
树的遍历有三条搜索路径:
- 先根遍历:若树不空,则先访问根结点,然后 依次先根遍历各棵子树
- 后根遍历:若树不空,则先依次后根遍历各棵 子树,然后访问根结点。
- 层次遍历:若树不空,则自上而下自左至右 访问树中每个结点。
森林的遍历
森林是由三部分组成的:
- 森林中第一棵树的根节点
- 森林中第一棵树的子树森林
- 森林中其他树构成的森林
森林的遍历:
- 先序遍历:
若森林不空,则 访问森林中第一棵树的根结点
先序遍历森林中第一棵树的子树森林;
先序遍历森林中(除第一棵树之外)其 余树构成的森林。
即:依次从左至右对森林中的每一棵树进行 先根遍历。
2. 中序遍历
若森林不空,则 中序遍历森林中第一棵树的子树森林
访问森林中第一棵树的根结点;
中序遍历森林中(除第一棵树之外)其 余树构成的森林。
即:依次从左至右对森林中的每一棵树进行 后根遍历。
树的应用
定义树的类型:
typedef struct CSNode {
Elem data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
求树的深度:
int TreeDepth(CSTree T) {
if(!T)
return 0;
else {
h1 = TreeDepth(Y->firstchild);
h2 = TreeDepth(T->nextsibling);
return (max(h1+1,h2));
}
}
输出树中所有从根到叶子的路径:
void AllPath(BiTree T,Stack &S) {
//输出二叉树上从根到所有叶子结点的路径
if(T) {
Push(S,T->data);
if(!T->lchild && !T->rchild)
PrintStack(S);
else {
AllPath(T->lchild,S);
AllPath(T->rchild,S);
}
Pop(S);
}
}
void OutPath(CSTree T,Stack &S) {
//输出树中所有的从根结点到叶子的路径
while(T) {
Push(S,T->data);
if(!T->firstchild)
PrintStack(S);
else
OutPath(T->firstchild,s);
Pop(S);
T = T->nextsiblling;
}
}
建树的存储结构的算法
和二叉树类似,不同的定义相应有不同的算法。
假设以二元组(F,C)的形式自上而下、自左而右的依次输入树的各边,建立树的孩子-兄弟链表
void CreatTree(CSTree &T) {
T = NULL;
for(scanf(&fa,&ch);ch!='#';scanf(&fa,&ch)) {
p = GetTreeNode(ch); //创建结点
EnQueue(Q,p); //指针入队列
if(fa == '#') T = p; //所建为根节点
else { //非根节点情况
GetHead(Q,s); // 取队列头元素(指针值)
while(s->data !=fa ) { // 查询双亲结点
DeQueue(Q,s);
GetHead(Q,s);
}
if(!(s->firstchild)) {
s->firstchild = p;
r = p;
} // 链接第一个孩子结点
else{
r->nextsibling = p;
r = p;
} // 链接其它孩子结点
}
}
}
赫夫曼树及其应用
最优树(赫夫曼树)的定义
结点的路径长度定义为:
从根节点到该节点的路径上分支的数目
树的路径长度定义为:
树中每个结点的路径长度之和
树的带权路径长度定义为:
树中所有叶子结点的带权路径长度之和:
例如:‘
在所有含n 个叶子结点、并带相同权 值的m 叉树中,必存在一棵其带权路径 长度取最小值的树,称为“最优树”。
如何构造最优树
以二叉树为例:
- 根据给定的n 个权值{w1, w2, …, wn}, 构造n 棵二叉树的集合 F= {T1, T2, … , Tn}, 其中每棵二叉树中均只含一个带权值 为wi 的根结点,其左、右子树为空树
- 在F 中选取其根结点的权值为最 小的两棵二叉树,分别作为左、 右子树构造一棵新的二叉树,并 置这棵新的二叉树根结点的权值 为其左、右子树根结点的权值之 和
- 从F中删去这两棵树,同时加入 刚生成的新树
- 重复(2)和(3)两步,直至F 中只 含一棵树为止
前缀编码
指的是,任何一个字符的编码都 不是同一字符集中另一个字符的编码 的前缀。
利用赫夫曼树可以构造一种不等长 的二进制编码,并且构造所得的赫夫 曼编码是一种最优前缀编码,即使所 传电文的总长度最短。
思想:
根据字符出现频率编码,使电文总 长最短
编码:
根据字符出现频率构造Huffman树, 然后将树中结点引向其左孩子的分支标 “0”,引向其右孩子的分支标“1”;每 个字符的编码即为从根到每个叶子的路径 上得到的0、1序列。