树与二叉树
个人见解
- 二叉树是由n个结点构成的有限集(n≥0),n=0时为空树,n>0时为非空树。
- 二叉树的结构是非线性的。
- 树中节点的度不大于2的有序树,它是一种最简单且最重要的树。.
- 二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树。
- 子树有左右之分,次序不能颠倒。
- 左子树和右子树同样都是二叉树。
- 满二叉树:一个二叉树的每一层的结点数都达到最大值。如果树的层数为k,则结点总数为-1。
- 完全二叉树:对于深度为k,且有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时称为完全二叉树。叶子结点只可能出现在最后两层。度为1的结点个数为0或1。
- 满二叉树的顺序表示:从上到下、从左到右进行编号(1,2,3,4,5,...,n)。
- 满二叉树是一种特殊的完全二叉树。
- 结点的度:一个结点的子树个数,每个结点只能含义0、1或2个孩子。
- 叶结点:度为0的结点,又称非终端结点。
- 结点的层次:层数。
- 树的度:树中所有结点的度的最大值。
- 树的高度:树中所有结点的层次的最大值。
方面理解,见图明意
二叉树的性质
- 一棵非空二叉树的第 i 层上最多有 个结点(i>=1)
- 若规定根节点的层数为1,则深度为 h 的二叉树的最大结点数是 (h>=1)
- 对任何一棵二叉树, 如果度为0其叶结点个数为 , 度为2的分支结点个数为,则有 = +1
- 具有n个结点的满二叉树的深度h = ⌊log2n⌋+1或h=
具有n个结点的完全二叉树的深度h = ⌊log2n⌋+1 (取整)
对于具有n个结点的完全二叉树,如果按照从上至下,从左至右的数组顺序对所有结点从1开始编号,则对于序号为i的结点有:
- i = 1,i 为根节点编号,无双亲节点。若 i > 1 ,i 位置节点的双亲序号为⌊i/2⌋
- 若 2i > n,则序号 i 没有左孩子;若 2i < n ,则序号 i 的左孩子的序号为 2i
- 若 2i +1 > n,则序号 i 没有右孩子;若 2i + 1 < n ,则序号 i 的右孩子的序号为 2i +1
存储结构
顺序存储
采用数组存储,一般使用数组存储只适合表示完全二叉树,因为不是完全二叉树会存在空间浪费。而现实使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一棵二叉树。
链式存储
用链表来表示一棵二叉树,即用链表来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域。左右指针分别用来给出该结点左右孩子所在的链结点的存储地址。此结点结构形成的二叉树称为二叉树链表。
为方便找到双亲结点,增加一个Parent域,指向该节点的双亲结点,此结点结构的存放方式称为三叉链表存储结构。
若一个二叉树含有n个结点,则二叉树链表中必定含有2n个指针域,n+1个空的链域。
链表结构的实现
由于链式二叉树结构的多变性,我们往往需要根据一棵二叉树的遍历序列来创建二叉树。这里先说明二叉树的遍历。
二叉树的遍历
二叉树的遍历是指按一定规律对二叉树中的每个结点进行访问且仅访问一次。这里的访问可以是计算二叉树中的结点数据,打印该结点的信息,也可以是对结点进行的任何其他操作。
遍历的目的:将非线性化结构变成线性化的访问序列。
递归算法的时间复杂度为O(n):设二叉树有n个结点,对每个结点都要进行一次入栈和出栈的操作,即入栈和出栈各执行n次,对结点的访问也是n次。
前序遍历:根->左->右
中序遍历:左->根->右
后序遍历:左->右->根
完整代码
线索二叉树
增设两个标志域 Ltag 和 Rtag
Ltag=0,LChild 域指向左孩子
Ltag=1,LChild 域指向遍历前驱
Rtag=0,RChild 域指向右孩子
Rtag=1,RChild 域指向遍历后继
对二叉树以某种次序进行遍历并且加上线索的过程称为线索化。指向前驱和后继结点的指针称为线索。这样的二叉链表称为线索链表。
完整代码
树、森林和二叉树的关系
树的存储结构
双亲表示法
用一组连续的空间来存储树中的结点,在保存每个结点的同时附设一个指示器来指示其双亲结点在表中的位置。其中data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组中的下标。
这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。
孩子表示法
把每个结点的孩子结点排列起来构成一个单链表,称为孩子链表。n个结点共有n个孩子链表(叶子结点的孩子链表为空表),而n个结点的数据和n个孩子链表的头指针又组成一个顺序表。
每个结点指针域的个数等于该结点的度。其中data为数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各个孩子的结点。
表头数组的表头结点:其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点的孩子链表的头指针。
孩子链表的孩子结点:其中child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针。
孩子兄弟表示法
又称为树的二叉表示法或二叉链表表示法,即以二叉链表作为树的存储结构。链表中每个结点设有两个链域,分别指向该结点的第一个孩子结点(左孩子)和下一个兄弟(右兄弟)结点。
其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址
相互转换
树 => 二叉树
将一棵树转换为二叉树的方法是:
①树中所有相邻兄弟之间加一条连线。
②对树中的每个结点,只保留其与第一个孩子结点之间的连线,删去其与其他孩子结点之间的连线。
③以树的根结点为轴心,将整棵树顺时针旋转-定的角度,使之结构层次分明。
- 转换所构成的二叉树是唯一的。
- 变换后的二叉树的根结点没有右孩子。
- 事实上,一 棵树采用孩子兄弟表示法所建立的存储结构与它所对应的二叉树的二叉链表存储结构是完全相同的,只是两个指针域的名称及解释不同而已。
森林 => 二叉树
森林可以用孩子兄弟链表表示。
森林转换为二叉树的方法如下:
①将森林中的每棵树转换成相应的二叉树。
②第一棵二叉树不动,从第二棵二叉树开始依次把后一棵二叉树的根结点作为前一棵二又树根结点的右孩子,当所有二叉树连在一起后,所得到的二叉树就是由森林转换得到的二叉树。
二叉树 => 树或森林
将一棵二叉树还原为树或森林,具体方法如下:
①若某结点是其双亲的左孩子,则把该结点的右孩子、右孩子的右孩.....都与该结点的双亲结点用线连起来。
②删掉原二叉树中所有双亲结点与右孩子结点的连线。
③整理由步骤①、②得到的树或森林,使之结构层次分明。
遍历
树的遍历
树的遍历结果与由树转换而得的二叉树有如下对应关系:
- 树的先根遍历 <=> 转换后二叉树的先序遍历
- 树的后根遍历 <=> 转换后二叉树的中序遍历
树的遍历方法主要有以下两种:
先根遍历
若树非空,则遍历方法为:
①访问根结点;
②从左到右,依次先根遍历根结点的每一棵子树。例如,图6.28中树的先根遍历序列为ABECFHGD。
后根遍历
若树非空,则遍历方法为:
①从左到右,依次后根遍历根结点的每一棵子树;
②访问根结点。例如,图6.28中树的后根遍历序列为EBHFGCDA。
森林的遍历
森林的先序遍历、中序遍历和后序遍历与其相应二叉树的先序遍历、中序遍历和后序遍历是对应相同的。
森林的遍历方法主要有以下三种:
先序遍历
若森林非空,则遍历方法为:
①访问森林中第一棵树的根结点;
②先序遍历第一棵树的根结点的子树森林;
③先序遍历除去第一棵树之后剩余的树构成的森林。例如,图6. 31(a)中森林的先序遍历序列为ABCDEFGHIJ。
中序遍历
若森林非空,则遍历方法为:
①中序遍历森林中第一棵树的根结点的子树森林;②访问第一棵树的根结点;
③中序遍历除去第一棵树之后剩余的树构成的森林。例如,图6.31(a)中森林的中序遍历序列为BCDAFEHJIG。
后序遍历
若森林非空,则遍历方法为:
①后序遍历森林中第一棵树的根结点的子树森林;②后序遍历除去第一棵树之后剩余的树构成的森林;
③访问第一棵树的根结点。
例如,图6.31(a)中森林的后序遍历序列为DCBFJIHGEA。
哈夫曼树
个人理解
路径:从一个结点到另一个结点的分支序列。
路径长度(PL):从一个结点到另一个结点所经过的分支数目。
树的路径长度:从根结点到每个结点的路径长度之和。
结点的权:给树的每个结点赋予一个具有某种实际意义的实数。
结点的带权路径长度:从树根到某一结点的路径长度与该结点的权的乘积。
树的带权路径长度(WPL):树中从根到每个叶子结点的带权路径长度之和。
n为叶子结点的个数,为第i个叶子结点的权值,为第i个叶子结点的路径长度。
完全二叉树具有最短路径长度的性质,但有些二叉树具备此性质,却不是完全二叉树。
哈夫曼树:由n个带权叶子结点构成的所有二叉树中带权路径长度最短的二叉树,又称最优二叉树。没有度为1的结点,因此一颗有n个叶子的哈夫曼树共有个结点。利用哈夫曼树,可以得到平均长度最短的编码。
构建哈夫曼树
算法步骤:
①初始化:用给定的n个权值。构建n棵二叉树并构成森林,其中每棵二叉树 T(≤i≤n) 都只有一 个权值为 的根结点,其左、右子树为空。
②找最小树:在森林F中选择两棵根结点权值最小的二又树,作为一棵新二叉树的左、右子树。标记新二叉树的根结点权值为其左、右子树的根结点权值之和。
③删除与加人:从F中删除被选中的那两棵二叉树,同时把新构成的二叉树加入森林F。
④判断:重复②、③操作,直到森林中只含有一棵二叉树为止,此时得到的这棵二叉树就是哈夫曼树。
存储结构:
每个结点的结构如图6.37所示
各结点存储在一维数组中,0号单元不使用,从1号位置开始使用。图6.38给出了一棵二叉树及其静态三叉链表。
算法实现
哈夫曼编码
(1)前缀编码
- 如果在一个编码系统中,任一编码都不是其他任何编码的前缀(最左子串),则称该编码系统中的编码是前缀编码。例如,一组编码“01, 001, 010, 100, 110”就不是前缀编码,因为“01”是“010”的前缀,若去掉“01”或“010”则这组编码就是前缀编码。
- 若是前缀编码,则在电文中各字符对应的编码之间不需要分隔符。如果不是前缀编码,则必须使用分隔符,否则会产生二义性。
(2)哈夫曼编码
- 对一棵具有n个叶子结点的哈夫曼树,若对树中的每个左分支赋予1,右分支赋予0(也可规定左0右1),则从根到每个叶子结点的通路上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。
- 哈夫曼编码是最优前缀编码。
图(graph)
个人理解
V中的数据元素称为顶点(vertex)
DataObject为一个集合,该集合中的所有元素具有相同的特性
VR是两个顶点之间关系的集合
P(x,y)表示x和y之间有特定的关联属性P
- 若<x,y>∈VR,则<x,y>表示从顶点x到顶点y的一条弧(arc),并称x为弧尾(ail)或起始点,称y为弧头(head)或终端点,此时图中的边是有方向的,称这样的图为有向图。
- 若<x,y>∈VR,必有<y,x>∈VR,即VR是对称关系,这时以无序对(x,y)来代替两个有序对,表示x和y之间的一条边 ( edge) ,此时的图称为无向图
无向图:设有n个顶点,边e的取值范围是0~。
有向图:设有n个顶点,边e的取值范围是0~。
完全图:设有n个顶点,e条边或弧,对于无向图,边e=。对于有向图,边e=。
稀疏图:边()很少的图。
稠密图:边()的图。
赋权图或网:带权的图,带权有向(无向)图又称为有向(无向)网。
子图:设有两个图 G=(V,{E}) 和图 G'=(V',{E'}),若且,则称图G'为G的子图。
领接点:
- 对于无向图 G=(V,{E}),如果,则称顶点 v,v' 互为邻接点,即 v,v' 相邻接。边(v,v')依附于顶点v,v',或者说边(v,v')与顶点口v,v'相关联。
- 对于有向图 G=(V,{A}),若弧,则称顶点 v邻接到顶点v',顶点v'邻接自顶点v,或者说边<v,v'>与顶点口v,v'相关联。
度(degree):
- 对于无向图,与v相关联的边的数目,记作TD(v)。
- 对于有向图,顶点v的度有出度和入度两部分,顶点v的度为TD(v)= ID(v)+OD(v)。
入度(in-degree):以顶点v为弧头的弧的数目,记作ID(v)。
出度(out-degree):以顶点v为弧尾的弧的数目,记作OD(v)。
度=边x2
路径:从顶点v到顶点v'的顶点序列。
路径长度:从一个顶点到另一个顶点所经过的边或弧的数目。
回路或环:在一个路径中,第一个顶点和最后一个顶点相同。
简单路径:表示路径的顶点序列中的顶点各不相同。
简单回路:表示路径的顶点序列中,除了第一个顶点和最后一个顶点相同,其余的顶点各不相同。
连通图:任意两个顶点都是连通的。
连通分量:无向图的极大连通子图。
强连通图:有向图中,,都是连通的。
强连通分量:有向图的极大连通子图。
一个连通图的生成树:是一个极小连通子图,它含有图中全部顶点,但只有足以连通n个点的n-1条边。
图的遍历
图的遍历就是从图中的某个顶点出发,按某种方法对图中的所有顶点访问且仅访问一次。图的遍历算法是求解图的连通性问题、拓扑排序和关键路径等问题的基础。为了保证图中各顶点在遍历过程中访问且仅访问一次,需要为每个顶点设一个访问标志,即为图设置一个访问标志数组 visited[n],用于标示图中每个顶点是否被访问过。该数组初值为0(假),一旦顶点v;访问过,则置 visited[i]为 1(真),表示该顶点已访问。
图的遍历通常有两种方法:深度优先搜索和广度优先搜索。这两种遍历方法对于无向图和有向图均适用。
深度优先搜索(depth-first search,DFS)
按照深度方向搜索,它类似于树的先根遍历,是树的先根遍历的推广。
算法思想:
- 从图中某个顶点出发,首先访问 。
- 找出刚访问过的顶点的第一个未被访问的邻接点,然后访问该邻接点。以该邻接点为新顶点,重复此步骤,直到刚访问过的顶点没有未被访问的邻接点为止。
- 返回前一个访问过且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点并访问,然后执行步骤 2 。
- 顶点A的未访邻接点有 B、D、E,首先访问A的第一个未访邻接点B。
- 顶点B的未访邻接点有 C、D,首先访问B的第一个未访邻接点C。
- 顶点C的未访邻接点只有F,访问 F。
- 顶点F没有未访邻接点,回溯到C。
- 顶点C已没有未访邻接点,回溯到B。
- 顶点B的未访邻接点只剩下D,访问D。
- 顶点D的未访邻接点只剩下G,访问G。
- 顶点G的未访邻接点有E、H,首先访问G的第一个未访邻接点E。
- 顶点E没有未访邻接点,回溯到G。
- 顶点G的未访邻接点只剩下H,访问H。
- 顶点H的未访邻接点只有I,访问I。
- 顶点I没有未访邻接点,回溯到H。
- 顶点H已没有未访邻接点,回溯到G。
- 顶点G已没有未访邻接点,回溯到D。
- 顶点D已没有未访邻接点,回溯到B。
- 顶点 B已没有未访邻接点,回溯到A。
- 至此,深度优先搜索过程结束,相应的访问序列为 ABCFDGEHI。
广度优先搜索(breadth-first search,BFS)
按照广度方向搜索,它类似于树的层次遍历,是树的按层次遍历的推广。
算法思想:
- 从图中某个顶点出发,首先访问。
- 依次访问的各个未被访问的邻接点。
- 分别从这些邻接点(端结点)出发,依次访问它们的各个未被访问的邻接点(新的端结点)。访问时应保证:如果 和为当前端结点,且在之前被访问,则的所有未被访问的邻接点应在的所有未被访问的邻接点之前访问。
- 重复3,直到所有端结点均没有未被访问的邻接点为止。若此时还有顶点未被访问,则选一个未被访问的顶点作为起始点,重复上述过程,直至所有顶点均被访问过为止。
- 顶点 A 的未访邻接点有 B、D、E,首先访问 A 的第一个未访邻接点 B。
- 访问A的第二个未访邻接点 D。
- 访问A的第三个未访邻接点 E。
- 由于B在D、E之前被访问,故接下来应访问B的未访邻接点。B的未访邻接点只有C,所以访问 C。
- 由于D在E、C之前被访问,故接下来应访问D的未访邻接点。D的未访邻接点只有G所以访问 G。
- 由于E在C、G之前被访问,故接下来应访问E的未访邻接点。E 没有未访邻接点,所以直接考虑在 E之后被访问的顶点C,即接下来应访问C的未访邻接点。C的未访邻接点只有F,所以访问 F。
- 由于G在F之前被访问,故接下来应访问G的未访邻接点。G的未访邻接点只有H,所以访问H。
- 由于F在H之前被访问,故接下来应访问F的未访邻接点。F没有未访邻接点,所以直接考虑在F之后被访问的顶点H,即接下来应访问H 的未访邻接点。H 的未访邻接点只有I,所以访问I。
- 至此,广度优先搜索过程结束,相应的访问序列为 ABDECGFHI。
1