树的基本概念
树(tree)是由结点或顶点和边组成的(可能是非线性的)且不存在着任何环的一种数据结构。没有结点的树称为空(null或empty)树。一棵非空的树包括一个根结点,还(很可能)有多个附加结点,所有结点构成一个多级分层结构。
树(tree)是包含n(n>=1)个结点,(n-1)条边的有穷集,其中:
- 每个元素称为结点(node);
- 有且只有一个特定的结点被称为根结点或树根(root)。
- 除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
结点
- 结点的度(degree):结点拥有的子树数
- 叶(子)结点(leaf)或终结点:度为0的结点
- 非终端结点或分支结点:度不为0的结点
- 内部结点:除根节点外,分支结点也称为内部结点
- 树的度:树内各结点的度的最大值
结点间关系
- 孩子(child):结点的子树的根称为该结点的孩子
- 双亲(parent):该结点称为孩子的双亲(父母同体,唯一的一个)
- 兄弟(sibling):同一个双亲的孩子之间互称兄弟
- 祖先:结点的祖先是从根到该结点所经分支上的所有结点
- 子孙:以某结点为根的子树中的任一结点都称为该节点的子孙
树的其他概念
- 层次(level):从根开始定义起,根为第一层,根的孩子为第二层
- 即:若某结点在第L层,则其子树的根就在第L+1层
- 堂兄弟:双亲在同一层的结点互为堂兄弟
- 深度(depth)或高度:树中结点的最大层次称为树的深度或高度
- 有序树/无序树:如果将树中结点的各子树看成从左至右有次序的,不能互换的,则称该树为有序树,否则为无序树。
- 森林(forest):m(m>=0)棵互不相交的树的集合。
- 对于树中每个结点而言,其子树的集合即为森林。
- 对于树中每个结点而言,其子树的集合即为森林。
线性结构 | 树结构 |
---|---|
第一个数据元素:无前驱 | 根结点:无双亲,唯一 |
最后一个数据元素:无后继 | 叶结点:无孩子,可以多个 |
中间元素:一个前驱,一个后继 | 中间结点:一个双亲,多个孩子 |
树的抽象数据类型
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
树的存储结构
双亲表示法
引入:除根节点外,其余每个结点,不一定有孩子,但一定有且仅有一个双亲
定义:设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。
- data:数据域,存储结点的数据信息
- parent:指针域,存储该结点的双亲在数组中的下标
约定:根节点的位置域为-1
孩子表示法
孩子表示法
多重链表表示法:
每个结点有多个指针域,其中每个指针指向一棵子树的根节点,这种方法叫做多重链表表示法。
方案一:设置指针域的个数为树的度
可能存在空间的浪费
方案二:设置每个结点指针域的个数等于该结点的度,取一个位置来存储结点指针域的个数
空间利用率提高,但是各个结点的链表结构不同,要维护结点的度的数值,时间损耗提高
孩子兄弟表示法
二叉树的相关概念
二叉树特点
特殊二叉树
斜树
- 所有结点都只有左子树的二叉树叫左斜树
- 所有结点都只有右子树的二叉树叫右斜树
- 这两者统称为斜树。
- 特点:每层只有一个结点,结点个数与二叉树的深度相同
注:线性表结构可以理解为是树的一种极其特殊的表现形式
满二叉树
定义:一棵二叉树中,所有分支结点都存在左右子树,并且所有叶子都在同一层
完全二叉树
定义:对一棵具有n个结点的二叉树按层序编号,如果编号i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则此二叉树为完全二叉树
完全二叉树
不完全二叉树
特点
- 叶子结点只能出现在最下层和次下层。
- 最下层的叶子结点集中在树的左部。
- 倒数第二层若存在叶子结点,一定在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即没有右子树。
- 同样结点数目的二叉树,完全二叉树深度最小。
满二叉树一定是完全二叉树,但反过来不一定成立。
二叉树的性质
下面的性质基本可以通过归纳法或者画图推出
-
二叉树性质1
性质1:在二叉树的第i层上至多有2i-1个结点(i>=1) -
二叉树性质2
性质2:深度为k的二叉树至多有2k-1个结点(k>=1) -
二叉树性质3
性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2+1。
一棵二叉树,除了终端结点(叶子结点),就是度为1或2的结点。假设n1度为1的结点数,则数T 的结点总数n=n0+n1+n2。我们再换个角度,看一下树T的连接线数,由于根结点只有分支出去,没有分支进入,所以连接线数为结点总数减去1。也就是n-1=n1+2n2,可推导出n0+n1+n2-1 = n1+2n2,继续推导可得n0 = n2+1。
- 二叉树性质4
性质4:具有n个结点的完全二叉树的深度为[log2n ] + 1([X]表示不大于X的最大整数)。
由性质2可知,满二叉树的结点个数为2k-1,可以推导出满二叉树的深度为k=log2(n + 1)。对于完全二叉树,它的叶子结点只会出现在最下面的两层,所以它的结点数一定少于等于同样深度的满二叉树的结点数2k-1,但是一定多于2k-1 -1。因为n是整数,所以2k-1 <= n < 2k,不等式两边取对数得到:k-1 <= log2n <k。因为k作为深度也是整数,因此 k= [log2n ]+ 1。
- 二叉树性质5
性质5:如果对一颗有n个结点的完全二叉树(其深度为[log2n ] + 1)的结点按层序编号(从第1层到第[log2n ] + 1层,每层从左到右),对任一结点i(1<=i<=n)有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。
- 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点i。
- 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
二叉树的存储结构
二叉树顺序存储结构
对于右斜树,顺序存储结构浪费存储空间
缺点
不能反应逻辑关系;
对于特殊的二叉树(左斜树、右斜树),浪费存储空间。
所以二叉树顺序存储结构一般只用于完全二叉树。
二叉链表
链表每个结点包含一个数据域和两个指针域
其中data是数据域,lchild和rchild都是指针域,分别指向左孩子和右孩子
/*二叉树的二叉链表结点结构定义*/
typedef struct BiNode
{
char data; /*结点数据*/
struct BiNode *lchild, *rchild; /*左右孩子指针*/
}BiNode,*BiTree;
三叉链表
增加一个指向其双亲的指针域
遍历二叉树的四种方式
先(前)序遍历
先访问根结点,然后前序遍历左子树,再前序遍历右子树
记忆:根左右
中序遍历
从根结点开始(不是访问根结点),中序遍历根结点的左子树,然后访问根节点,最后中序遍历右子树
记忆:左根右
后序遍历
从左到右先叶子后结点遍历左右子树,,最后访问根结点
记忆:左右根
层序遍历
从根结点开始访问,从上向下逐层遍历,在同一层中,按从左到右顺序对结点访问
记忆:根上下左右
建立二叉树
二叉树的扩展二叉树:
为了能让每个结点确认是否有左右孩子,将每个结点的空指针引出一个虚结点,其值为一特定值,比如"#"
称这种处理后的二叉树为原二叉树的扩展二叉树
扩展二叉树就可以做到一个遍历序列确定一棵二叉树
线索二叉树
在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
树、森林、二叉树之间的相互转换
树转换为二叉树
由于二叉树是有序的,为了避免混淆,对于无序树,我们约定树中的每个结点的孩子结点按从左到右的顺序进行编号。
将树转换成二叉树的步骤是:
(1)加线。就是在所有兄弟结点之间加一条连线;
(2)抹线。就是对树中的每个结点,只保留他与第一个孩子结点之间的连线,删除它与其它孩子结点之间的连线;
(3)旋转。就是以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。这里的旋转需要注意,左边的成为左子树,右边的成为右子树。
森林转换为二叉树
森林是由若干棵树组成,可以将森林中的每棵树的根结点看作是兄弟,由于每棵树都可以转换为二叉树,所以森林也可以转换为二叉树。
将森林转换为二叉树的步骤是:
(1)先把每棵树转换为二叉树;
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子结点,用线连接起来。当所有的二叉树连接起来后得到的二叉树就是由森林转换得到的二叉树。
二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,其步骤是:
(1)若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结点的右孩子结点……都作为该结点的孩子结点,将该结点与这些右孩子结点用线连接起来;
(2)删除原二叉树中所有结点与其右孩子结点的连线;
(3)整理(1)和(2)两步得到的树,使之结构层次分明。
二叉树转换为森林
假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。
(1)先把每个结点与右孩子结点的连线删除,得到分离的二叉树;
(2)把分离后的每棵二叉树转换为树;
(3)整理第(2)步得到的树,使之规范,这样得到森林。
赫(哈)夫曼树、哈夫曼编码
历史
贴下哈夫曼编码的发明史,膜拜一下。
1951年,哈夫曼在麻省理工学院(MIT)攻读博士学位,他和修读信息论课程的同学得选择是完成学期报告还是期末考试。导师罗伯特·法诺(Robert Fano)出的学期报告题目是:查找最有效的二进制编码。由于无法证明哪个已有编码是最有效的,哈夫曼放弃对已有编码的研究,转向新的探索,最终发现了基于有序频率二叉树编码的想法,并很快证明了这个方法是最有效的。哈夫曼使用自底向上的方法构建二叉树,避免了次优算法香农-范诺编码(Shannon–Fano coding)的最大弊端──自顶向下构建树。
1952年,于论文《一种构建极小多余编码的方法》(A Method for the Construction of Minimum-Redundancy Codes)中发表了这个编码方法。
哈夫曼树相关概念
当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”。
遵循一个原则,那就是:权重越大的结点离树根越近。
路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。图中,从根结点到结点 a 之间的通路就是一条路径。
路径长度:在一条路径中,每经过一个结点,路径长度都要加 1 。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1 。图中从根结点到结点 c 的路径长度为 3。
结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权。例如,图 1 中结点 a 的权为 7,结点 b 的权为 5。
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。例如,图中结点 b 的带权路径长度为 2 * 5 = 10 。
树的带权路径长度为树中所有叶子结点的带权路径长度之和。通常记作 “WPL” 。例如图中所示的这颗树的带权路径长度为:WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3
构建哈夫曼树的过程
对于给定的有各自权值的 n 个结点,构建哈夫曼树有一个行之有效的办法:
- 在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和;
- 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推;
- 重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。
(A)给定了四个结点a,b,c,d,权值分别为7,5,2,4;第一步如
(B)所示,找出现有权值中最小的两个,2 和 4 ,相应的结点 c 和 d 构建一个新的二叉树,树根的权值为 2 + 4 = 6,同时将原有权值中的 2 和 4 删掉,将新的权值 6 加入;进入(C),重复之前的步骤。
(D)中,所有的结点构建成了一个全新的二叉树,这就是哈夫曼树。
小结
其实本节还是缺失了很多内容,比如红黑树、平衡二叉树、B树、B+树、B*树等,哈夫曼树和树的实现没有展开,但是赶下进度,暂时先这样了
还是顺手贴大佬的笔记
另外发现一个国人的算法项目,和leetcode联系较多。