一 数的基本概念
1 定义
数是n个(n$\geqslant$0)结点的有限集,n=0称为空树
(1)有且仅有一个结点称为根节点
(2)n>1时,其余结点可分为m(m>0)个互不相交的有限集,每个集合本身又是一颗数,并且称为根的子树
树是一种递归的数据结构,数是一种逻辑结构,也是一种分层结构,特点:
(1)根节点没有前驱,除根结点外的所有结点有且只有一个前驱
(2)所有结点都可以有零个或多个后继
n个结点的树有n-1条边
2 基本术语
(1)以k为例。ABE都是K的祖先,且E是K的父亲结点,K是E的孩子结点,F等结点是K的叔叔结点,L是K的兄弟结点
(2)树的一个结点的孩子个数称为该结点的度,结点的最大度数称为树的度,如D的度为3,树的度就是3
(3)度大于0的称为分支结点(非终端结点),度为0的称为叶结点(终端结点)
(4)结点的深度,高度和层次
层次:从树根开始定义,根节点是第一层或者第零层
深度:从根结点开始自顶向下逐层累加的
高度:从叶结点开始自底向上逐层累加的
高度或深度是树中结点的最大层数,图中高度为4
(5)有序树和无序树
各子树从左到右有次序不能互换,称为有序树,否则是无序树
(6)路径和路径长度
树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,路径长度是路径上所经过的边的个数
(7)森林
森林是m(m$\geqslant$0)棵互不相交的树的集合,把树的根结点删去就成了森林
3 树的性质
(1)树中的结点数等于所有结点的度数之和加1
(2)度为m的树中第i层上至多有==mi-1===个结点,i大于等于1
(3)高度为h的m叉树至多有==(mh-1)/(m-1)个结点==
(4)具有n个结点的m叉树的最小高度为logm(n(m-1)+1),往上取整
二 二叉树的概念
1 二叉树的定义和特性
1.1 定义
二叉树是有序树,即使树中结点只有一颗子树,也要区分它是左子树还是右子树
二叉树与度为2的有序树的区别:
(1)度为2的至少有三个结点,二叉树可以为空
(2)度为2的左右次序是相对另一孩子而言,若只有一个孩子,不需要区分左右次序,二叉树必须确定左右次序,是确定的
1.2 特殊的二叉树
(1)满二叉树
高度为h,结点数为2h-1的树为满二叉树
(2)完全二叉树
高度为h,有n个结点的二叉树
特点:若i ⩽ \leqslant ⩽ ⌊ \lfloor ⌊n/2 ⌋ \rfloor ⌋,则结点i为分支结点,否则为叶结点;叶结点只能出现在层次最大的两层,若有度为1的结点,则只可能有一个,且该结点只有左孩子;按层序编号后,一旦出现某个结点(编号为i)为叶结点或只有左孩子,则编号大于i的结点均为叶结点;若n为奇数,则每个分支结点都有左右孩子,若n为偶数,编号最大的分支结点(编号为n/2)只有左孩子,其余分支左右孩子都有
(3)二叉排序树
左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字;左右子树又各是一颗二叉排序树
(4)平衡二叉树
书上任意一个结点的左右子树的深度之差不超过1
1.3 二叉树的性质
(1)分空二叉树上的叶结点数等于度为2的结点数加1,即n0=n2+1
(2)非空二叉树上的第k层上至多有2k-1个结点
(3)高度为h的二叉树至多有2h-1个结点
(4)完全二叉树编号依次为1……n则有:
i>1时,结点i的父结点的编号为 ⌊ \lfloor ⌊i/2 ⌋ \rfloor ⌋,即i是偶数,双亲是i/2,i是左孩子;i是奇数,双亲是(i-1)/2,i是右孩子
2i ⩽ \leqslant ⩽n时,i的左孩子编号为2i,否则无左孩子
2i+1 ⩽ \leqslant ⩽n,i的右孩子编号为2i+1,否则无右孩子
i所在层次深度为 ⌊ \lfloor ⌊log2i ⌋ \rfloor ⌋+1
(5)具有n个结点的完全二叉树的高度为 ⌈ \lceil ⌈log2(n+1) ⌉ \rceil ⌉或者 ⌊ \lfloor ⌊log2n ⌋ \rfloor ⌋+1
2 二叉树的存储结构
2.1 顺序存储结构
用一组地址连续的存储单元一次自上而下,自左至右存储完全二叉树上的结点元素,即编号为i的存在下标为i-1的分量中
完全二叉树和满二叉树采用顺序存储比较合适
建议把0下标空出来,把下标和编号从1开始对齐,不然不满足刚刚的性质4
2.2 链式存储结构
顺序存储空间利用率低,因此二叉树一般采用链式存储
除了左右孩子指针,实际还可以增加如指向父结点的指针
typedef struct BiTnode
{
elemtype data;
struct BiTnode *lchild,*rchild;
}BiTnode,*BiTree;
在含有N个结点的二叉链表中,还有n+1个空链域
三 二叉树的遍历和线索二叉树
1 遍历
以上图的图5.6为例
非递归算法需要借助栈
1.1 先序遍历
递归算法:
void Preorder(Bitree T)
{
if(T!=NULL)
{
visit(T);
Preorder(T->Lchild);
Preorder(T->Rchild);
}
}
1 2 4 6 3 5
非递归算法:
void Preorder2(Bitree T)
{
Initstack(S);
Bitree p =T;
while(p|| !Isempty(S))
{
if(p)
{
visit(p);
Push(S,p);
p=p->lchild;
}
else
{
Pop(S,p);
p=p->rchild;
}
}
}
1.2 中序遍历
递归算法:
void Inorder(Bitree T)
{
if(T!=NULL)
{
Inorder(T->Lchild);
visit(T);
Inorder(T->Rchild);
}
}
2 6 4 1 3 5
非递归算法:
void Inorder2(Bitree T)
{
Initstack(S);
Bitree p =T;
while(p|| !Isempty(S))
{
if(p)
{
Push(S,p);
p=p->lchild;
}
else
{
Pop(S,p);
visit(p);
p=p->rchild;
}
}
}
1.3 后序遍历
递归算法:
void Postorder(Bitree T)
{
if(T!=NULL)
{
Postorder(T->Lchild);
Postorder(T->Rchild);
visit(T);
}
}
6 4 2 5 3 1
时间复杂度都是O(n),空间复杂度O(n)
非递归算法(最难):
void Postorder2(Bitree T)
{
Initstack(S);
Bitree p =T;
Bitree r =NULL;
while(p|| !Isempty(S))
{
if(p)
{
Push(S,p);
p=p->lchild;
}
else
{
Gettop(S,p);
if(p->rchild && p->rchild!=r)
{
p=p->rchild;
}
else
{
Pop(S,p);
visit(p);
r=p;
p=NULL;
}
}
}
}
1.4 层次遍历
需要借助队列
void Levelorder(Bitree T)
{
Initqueue(Q);
Bitree p;
Enqueue(Q,T);
while(!Isempty(Q))
{
Dequeue(Q,p);
visit(p);
if(p->lchild!=NULL)
{
Enqueue(Q,p->lchild);
}
if(p->rchild!=NULL)
{
Enqueue(Q,p->rchild);
}
}
}
二叉树的先序序列和中序序列可以唯一确定一颗二叉树
二叉树的后序序列和中序序列可以唯一确定一颗二叉树
二叉树的层序序列和中序序列可以唯一确定一颗二叉树
2 线索二叉树
每个度为1的结点都有1个空指针,空指针总数为n0+n1+n2+1=n+1
能否利用这些空指针来存放其前驱和后继的指针?
这样可以像遍历单链表那样方便的遍历二叉树,引入线索二叉树就是为了加快查找结点前驱和后继的速度
规定:若无左子树,则lchild指向其前驱结点;若无右子树,rchild指向其后继结点
标志域的含义:
ltag=0,lchild指向左孩子,ltag=1,指向结点的前驱
rtag=0,rchild指向右孩子,rtag=1,rchild指向结点的后继
typedef struct Threadnode
{
elemtyoe data;
struct Threadnode *lchild,*rchild;
int ltag,rtag;
}Threadnode,*Threadtree;
2.1 中序线索二叉树的构造
二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索
通过中序遍历对二叉树线索化的递归算法如下:
void Inthread(Threadtree &p,Threadtree &pre)
{
if(p!=NULL)
{
Inthread(p->lchild,pre); \\递归 线索化左子树
if(p->lchild==NULL) \\左子树为空,建立前驱线索
{
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL && pre->rchild==NULL)
{
pre->rchild=p; \\建立前驱结点的后继线索
pre->rtag=1;
}
pre=p; \\标记当前结点成为刚刚访问过的结点
Inthread(p->rchild,pre); \\递归,线索化右子树
}
}
void CreateInthread(Threadtree T) \\主过程算法
{
Threadtree pre=NULL;
if(T!=NULL)
{
Inthread(T,pre);
pre->rchild=NULL;
pre->rtag=1;
}
}
为了方便可以添加一个头结点,二叉树中序序列中的第一个结点的lchild指针和最后一个结点的rchild指针均指向头结点,这类似建立了一个双向线索链表,可以从前往后或从后往前对线索二叉树进行遍历
2.2 中序线索二叉树的遍历
先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空
若其右标志为1,则右链为线索,指向后继,否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继
不含头结点的线索二叉树的遍历算法:
(1)求第一个结点
Threadnode *Firstnode(Threadnode *p)
{
while(p->ltag==0)
p=p->lchild;
return p;
}
(2)求p的后继
Threadnode *Nextnode(Threadnode *p)
{
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild;
}
(3)遍历算法
void Inorder(Threadnode *T)
{
for(Threadnode*p=Firstnode(T); p!=NULL; p=Nextnode(p))
visit(p);
}
2.3 先序线索二叉树和后序线索二叉树
怎么在先序线索二叉树中找结点的后继:如果有左孩子,则左孩子就是其后继,如果无左孩子但有右孩子,右孩子就是后继;如果是叶结点,则右链域直接指示了结点的后继
后序线索二叉树在结点的后继分为三种情况:
(1)若结点x是二叉树的根,则后继为空
(2)若结点x是其双亲的右孩子,或是左孩子且没有右子树,则后继就是双亲
(3)结点x是双亲的左孩子,且双亲有右孩子,则其后继为双亲的右子树上按后序遍历列出的第一个结点
四 树,森林
1 树的存储结构
1.1 双亲表示法
采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置
根结点下标为0,伪指针域为-1
#define Max_tree_size 100
typedef struct
{
elemtype data;
int parent;
}Ptnode;
typedef struct
{
Ptnode nodes[Max_tree_size];
int n ;
}Ptree;
二叉树都可以用树的存储结构来存储,但树却不能用二叉树的存储结构
1.2 孩子表示法
将每个结点的孩子结点都用单链表链接起来形成一个线性结构,n个结点就有n个孩子链表(叶结点的孩子链表为空表)
这种结构找子女非常直接,但是找双亲需要遍历n个结点中孩子链表指针域所指向的n个孩子链表
1.3 孩子兄弟表示法
又称为二叉树表示法
结构体包括:结点值,指向结点第一个孩子结点的指针,指向结点下一个兄弟结点的指针(可以找到结点的所有兄弟结点)
typedef struct CSnode
{
elemtype data;
struct CSnode *firstchild,*nextsibling;
}CSnode,*CStree;
可以很方便的实现树转换成二叉树的操作,找结点的孩子简单,找双亲麻烦,可以增设一个parent指针指向父结点
2 树,森林与二叉树的转换
树转换成二叉树:每个结点左指针指向他的第一个孩子,右指针指向它在树中的相邻右兄弟,又称为左孩子右兄弟
根结点没有兄弟,对应的二叉树没有右子树
森林转换成二叉树:把森林的每棵树转换成二叉树,把第二课树根视为第一颗树根的右兄弟,第三棵树对应的二叉树当作第二颗二叉树根的右子树,以此类推
二叉树转换成森林:若二叉树非空,二叉树的根机器左子树为第一颗树的二叉树形式,将根的右链断开;根的右子树又可以视为一个新的二叉树,继续断开右链直到没有右子树的二叉树为止
3 树和森林的遍历
树的遍历:
(1)先根遍历:先根后子树,与对应的二叉树的先序相同
(2)后根遍历:先子树后根,与对应二叉树的中序序列相同
森林的遍历:
(1)先序遍历
访问第一棵树的根结点
先序遍历第一棵树中根节点的子树森林
先序遍历除去第一棵树之后剩余的树构成的森林
(2)中序遍历
中序遍历森林中第一棵树的根节点的子树森林
访问第一棵树的根结点
中序遍历春去第一棵树之后剩余的树构成的森林
五 树与二叉树的应用
1 哈夫曼树(最优二叉树)和哈夫曼编码
1.1 定义
从树的根到任意结点的路径长度与该结点上的权值的乘积,称为该结点的带权路径长度
所有叶结点的带权路径长度之和称为该树的带权路径长度,记为WPL
WPL最小的二叉树称为哈夫曼树
1.2 构造
(1)将n个结点分别看做一个树,构成森林F
(2)构造一个新结点,从F中选两个权值最小的树作为左右子树,并且将新节点的权值置为左右子树上根结点的权值之和
(3)从F中删除刚刚两个子树,把新的树加入F
(4)重复23,直到剩下一个树
每个初始结点都成为叶结点,权值越小的结点到根结点的路径长度越大
新建了n-1个结点,节点总数为2n-1;不存在度为1的结点
1.3 哈夫曼编码
编码方式:固定长度编码和可变长度编码
哈夫曼编码就是一种非常有效的数据压缩编码(可变)
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。例如字符ABC对应的编码为0,101,100
哈夫曼树中,将字符的编码方式解释为从根至该字符的路径上边标记的序列,其中0表示转向左孩子,1表示转向右孩子,如下图
WPL=224
此处的WPL可以视为最终编码得到二进制编码的长度,共224位,若采用3位固定长度编码,则得到编码长度为300位,可见哈夫曼树压缩了数据
2 并查集
是一种简单的集合表示
Initial(S); \\将集合S中的每个元素都初始化为只有一个单元素的子集合
Union(S,root1,root2); \\ 把集合S中的子集合root2并入子集合root1,要求互不相交,否则不执行合并
Find(S,x); \\查找集合S中单元素x所在的子集合,并返回该子集合的根结点
通常用树的双亲表示作为并查集的存储结构,每个子集合一棵树
所有子集合的树构成森林,存放在双亲表示数组内
用数组元素的下标代表元素名,根结点的下标代表子集合名,根结点的双亲结点为负数
两个集合的并,只需将其中一个子集合根结点的双亲指针指向另一个集合的根结点
#define size 100
int UFsets[size];
void Initial(int S[])
{
for(int i =0;i<size;i++)
{
S[i]=-1;
}
}
int Find(int S[],int x)
{
while(S[x]>=0)
x=S[x];
return x;
}
void Union(int S[],int root1,int root2)
{
if(root1==root2)
return;
S[root2]=root1;
}