树是n(n>=0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:1.有且仅有一个特定的称为根的结点2.当n>1时其余结点可分为m(m>0)个互不相交的有限集T1,T2。。。。。Tn 其中每一个集合本身又是一棵树,并且称为根的子树。
结点拥有的子树数称为结点的度,度为0的结点称为叶结点或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点,树的度是树内各结点的度的最大值。
------A------
| |
--------B -----C-----
| | |
--------D------- E F
| | | |
G H I J
A为根节点 BCDE为内部结点 GHIJF为叶结点或终端结点
B结点度为1 C度为2 D结点度为3 J结点度为0
ABCDE 为分支结点或非终端结点
B是D的双亲 B是A的孩子 B和C互为兄弟
结点的层次从根开始 第一层 第二层 第三层 第四层
最大层次称为树的深度或高度 当前为4
双亲在同一层的结点互为堂兄弟 例如I J
如果将树中结点的各子树看成从左到右是有次序的,不能互换的,则称该树是有序树,否则是无序树。
森林是m(m>=0) 棵互不相交的树的集合,对树中每个结点而言,其子树的集合即为森林。 例如 B结点及以下 和C及以下结点 那么B C 即森林
树的抽象数据类型
ADT 树(tree)
Data
树是由一个根节点和若干棵子树构成。树中结点具有相同数据类型及层次关系
Operation
InitTree(*T):构造空树T
DestroyTree(*T):销毁树T
CreateTree(*T,definition):按definition中给出树的定义来构造树
ClearTree(*T):若树T存在,则将树T清为空树。
TreeEmpty(T):若T树为空树,返回true,否则返回false
TreeDepth(T):返回T的深度
Root(T):返回T的根结点
Value(T,cur_e):cur_e是树T中的一个结点,返回此结点的值
Assign(T,cur_e,value):给树T的结点cur_e赋值为value
Parent(T,cur_e):若cur_e是树T的非根结点,则返回它的双亲,否则返回空。
LeftChild(T,cur_e):若cur_e是树T的非叶结点,则返回它的最左孩子,否则返回空。
RightSibling(T,cur_e):若cur_e有右兄弟,则返回它的右兄弟,否则返回空
InsertChild(*T,*p,i,c):其中p指向树T的某个结点,i为所指结点p的度加上1,非空树c与T不相交,操作结果为插入c为树T中p指结点的第i棵子树
DeleteChild(*T,*p,i):其中p指向树T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点的第i棵子树
endADT
树的存储结构
1.双亲表示法
在每个结点中,附设一个指示器指示其双亲结点到链表中的位置
data parent
其中data是数据域,存储节点的数据信息,而parent是指针域,存储节点的双亲在数组中的下标
树的双亲表示法结点结构定义。
#define MAX_TREE_SIZE 100
typedef int TElemType; 树结点的数据类型,目前暂定为整型
typedef struct PTNode 结点结构
{
TElemType data; 结点数据
int parent; 双亲位置
}PTNode;
typedef struct 树结构
{
PTNode nodes[MAX_TREE_SIZE]; 结点数组
int r,n; 根的位置和结点数
}PTree;
由于根结点是没有双亲的,所以我们约定根结点的位置域设置是-1.
下标 data parent
0 A -1
1 B 0
2 C 0
3 D 1
4 E 2
可以根据parent指针很容易找到双亲结点,所以O(1),如果是找结点的孩子,那就要遍历整个结构。
我们关注结点的双亲,结点孩子,结点兄弟。可以扩展双亲域,长子域,右兄弟域。
存储结构的设计是一个非常领过的过程,一个存储结构设计的是否合理,取决于基于该存储结构的运算是否适合,是否方便,时间复杂度好不好等。
2.孩子表示法
每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。
方案一
指针域的个数就等于树的度,复习一下,树的度是树各结点度的最大值。
data child1 child2 child3 child4 child5 。。。
data是数据域,child到childn 是指针域指向该结点的孩子结点。
因为各个结点的度不一样 所以导致有些结点度少的空了很多指针域
方案二
每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数。
data degree child1 child2 child3 。。。。。childn
data为数据域 degree为度域 也就是存储该结点的孩子结点的个数 child1到childd为指针域,指向各个结点孩子的结点。
这种方法克服了浪费空间的缺点,对空间利用率提高了。
以上都是有些不足
孩子表示法:把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。
下标 data firstchild child next
0 A 1 2
1 B 3
2 C 4 5
3 D 6 7 8
4 E 9
5 F
6 G
7 H
8 I
9 J
为此,设计两种结点结构,一个是孩子链表的孩子结点。
child next
其中child是数据域,用来存储某个结点在表头数组中的下标,next是指针域,用
来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点
data firstchild
其中data是数据域,存储某结点的数据信息,firstchild是头指针域,存储该结点的孩子链表的头指针。
树的孩子表示法结构定义
#define MAX_TREE_SIZE 100
typedef struct CTNode 孩子结点
{
int child;
struct CTNode*next;
}*ChildPtr;
typedef struct 表头结构
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct 树结构
{
CTBox nodes[MAX_TREE_SIZE]; 结点数组
int r,n; 根的位置和结点数
}CTree;
把双亲表示法和孩子表示法结合 双亲孩子表示法,即在上面多一个parent一列
下标 data parent firstchild
孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
data firstchild rightsib
data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址
树的孩子兄弟表示法结构定义
typedef struct CSNode
{
TElemType data;
struct CSNode * firstchild,*rightsib;
}CSNode,*CSTree;
二叉树的定义
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的,分别称为根结点的左子树和右子树的二叉树组成。
二叉树的特点:
1.每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点,注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的
2.左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手,双脚,但显然左手,左脚和右手,右脚是不一样的,右手戴左手套,右脚穿左鞋会极其别扭
3.即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树具有五种基本形态
1.空二叉树
2.只有一个根结点
3.根结点只有左子树
4.根结点只有右子树
5.根结点既有左子树又有右子树
如果有3个结点树只有2种情况
而如果是二叉树 则有5种情况
特殊二叉树:
1.斜树
所有的结点都只有左子树的二叉树叫左子树,所有结点都是只有右子树的二叉树叫做右斜树。这两种统称为斜树。
2.满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层,这样的二叉树称为满二叉树
单是每个结点都存在左右子树,不能算是满二叉树,还必须要所有的叶子都在同一层,这就做到了整棵树的平衡。因此满二叉树的特点有:
1.叶子只能出现在最下一层。出现在其他层就不能达成平衡
2.非叶子结点的度一定是2.否则就是缺胳膊少腿了
3.在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
3.完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树编号为i的结点在二叉树中位置完全相同,则这课二叉树称为完全二叉树
完全二叉树的特点:
1.叶子结点只能出现在最下两层
2.最下层的叶子一定集中在左部连续位置
3.倒数二层,若有叶子结点,一定都在右部连续位置
4.如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
5.同样结点数的二叉树,完全二叉树的深度最小。
编号连续 逐层排号 如果断了 就不是完全二叉树
二叉树的性质
性质1:在二叉树的第i层上至多有2^i-1个结点(i>=1)。
性质2:深度为k的二叉树至多有(2^k)-1个结点(k>=1)
性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2.则n0=n2+1 n0是度为2的结点数 n2是度为0的结点数
性质4:具有n个结点的完全二叉树的深度为 以2为底n的对数+1
性质5:如果对一棵有n个结点的完全二叉树的结点按照层序编号(从第一层到第深度 那层 每层从左到右),对任一结点i(1<=i<=n)
1。如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点i/2
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
二叉树顺序存储结构
顺序存储一般只用于完全二叉树
二叉树链表
二叉树每个结点最多有2个孩子,所以为它设计一个数据域和两个指针域。这样的链表叫做二叉链表
lchild data rchild
data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针
二叉树的二叉链表结点结构定义
typedef struct BiTNode 结点结构
{
TElemType data; 结点数据
struct BiTNode*lchild,*rchild; 左右孩子指针
}BiTNode,*BiTree;
还可以增加一个指向其双亲的指针域,那样就称之为三叉链表。
遍历二叉树
二叉树遍历:是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树遍历方法
1.前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后在前序遍历左节点,再前序遍历右结点。
-------A--------
| |
B C-------
| | |
--------D E F
| | |
G H I
ABDGHCEIF
2.中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
GDHBAEICF
3.后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点。
GHDBIEFCA
4.层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
ABCDEFGHI
前序遍历算法
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data); 显示结点数据,可以更改为其他对结点操作
PreOrderTraverse(T->lchild); 再先序遍历左子树
PreOrderTraverse(T->rchild); 最后先序遍历右子树
}
中序遍历算法
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild);中序遍历左子树
print("%c",T->data); 显示结点数据,可以更改为其他对结点操作
InOrderTraverse(T->rchild); 最后中序遍历右子树
}
后序遍历算法
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild);先后序遍历左子树
PostOrderTraverse(T->rchild);在后序遍历右子树
Printf("%c",T->data);显示结点数据,可以更改为其他对结点操作
}
前序ABCDEF 中序 CBAEDF 后?CBEFDA
中序ABCDEFG 后序BDCAFGE 前?EACBDGF
二叉树的建立
为了每个结点确认是否有左右孩子,我们对它进行扩展,也就是将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如#,我们称这种处理后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。
例如 --------A-------
B C
--------- ---------
# D # #
--------
# #
前序遍历AB#D##C##
按前序输入二叉树中结点的值(一个字符)
#表示空树,构造二叉链表表示二叉树T
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c",&ch);
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; 生成根结点
CreateBiTree(&(*T)->lchild); 构造左子树
CreateBiTree(&(*T)->rchild); 构造右子树
}
}
线索二叉树
我们把指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树
我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化
为每个结点增设标志域ltag和rtag,存放0或1布尔型变量。
lchild ltag data rtag rchild
ltag为0时指向该结点的左孩子,为1时指向该结点的前驱
rtag为0时指向该结点的右孩子,为1时指向该结点的后继
线索二叉树结构实现
typedef enum{Link,Thread}PointterTag; Link==0表示指向左右孩子指针 Thread==1表示指向前驱或后继的线索
typedef struct BiThrNode 二叉线索存储结点结构
{
TElemType data; 结点数据
struct BiThrNode *lchild,*rchild; 左右孩子指针
PointerTag LTag;
PointerTag RTag; 左右标志
}BiThrNode,*BiThrTree;
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是遍历的过程中修改空指针的过程。
中序遍历线索化的递归函数:
BiThrTree pre;全局变量,始终指向刚刚访问过的结点
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild);递归左子树线索化
if(!p->lchild) 没有左孩子
{
p->LTag=Thread; 前驱线索
p->lchild=pre; 左孩子指针指向前驱
}
if(!pre->rchild) 前驱没有右孩子
{
pre->RTag=Thread; 后继线索
pre->rchild=p; 前驱右孩子指针指向后继(当前结点p)
}
pre=p; 保持pre指向p的前驱
InThreading(p->rchild);递归右子树索引
}
}
有了线索二叉树后,我们对它进行遍历时发现,其实就等于是操作一个双向链表结构。
和双向链表结构一样,在二叉树线索链表上添加一个头结点。并令其lchild域的指针指向二叉树的根结点,其rchild域的指针指向中序遍历时访问的最后一个结点。反之,令二叉树的中序序列中的第一个结点中,lchild域指针和最后一个结点的rchild域指针均指向头结点,这样定义的好处就是我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
T指向头结点,头结点左链lchild指向根结点,头结点右键rchild指向中序遍历的
最后一个结点,中序遍历二叉线索链表表示的二叉树T
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p=T->lchild;
while(p!=T)
{
while(p->LTag==Link)
p=p->lchild;
printf("%c",p->data);
while(p->RTag==Thread && p->rchild!=T)
{
p=p->rchild;
printf("%c",p->data);
}
p=p->rchild;
}
return OK;
}
如果所用的二叉树需要经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
树,森林与二叉树的转换
树转换为二叉树步骤:
1.加线。 在所有兄弟结点之间加一条线
2.去线。 对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3.层次调整。 以树的根结点为轴心,讲整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
森林转换成二叉树
1.把每个树转换成二叉树
2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一颗二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
二叉树转换为树
1.加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点,右孩子的右孩子结点,右孩子的右孩子的右孩子的结点,,右孩子结点都作为此结点的孩子。将该结点与这右孩子结点用线连接起来。
2.去线。 删除原二叉树中所有结点与其右孩子结点的连线
3.层次调整。 使之结构层次分明
二叉树转换为森林
判断一颗二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。那么如果是转换成森林:
1.从根节点开始,若右孩子存在,则把与右孩子结点的连线删除,在查看分离后的二叉树,若右孩子存在,则连线删除。。。。直到所有右孩子连线都删除为止,得到分离的二叉树。
2.再将每棵分离后的二叉树转换为树
树与森林的遍历
树的遍历分为两种方式:
1.一种是先根遍历树,即先访问树的根节点,然后依次先根遍历根的每棵子树。
2,另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。
森林的遍历分为两种方式:
1.前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,在依次用同样方式遍历除去第一棵树的剩余树构成的森林。
2.后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。
当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法实现。
霍夫曼树定义与原理
我们先把二叉树简化成叶子结点带权的二叉树。
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。
树的路径长度就是从树根到每一结点的路径长度之和。
结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。
转载于:https://blog.51cto.com/pankuo/1631337