树是 个结点的有限集。可以为空树 。
树是一种逻辑结构。
树适用于表示具有层次结构的数据。最多只和上一层的一个结点有直接关系。
祖先:从根结点到结点 的唯一路径上的所有除 以外的结点,称为 的祖先。
子孙:如果 是 的祖先,那么 就是 的子孙。
双亲/前驱:最接近 结点的祖先,就是 的双亲。
孩子/后继:如果 为 的双亲,那么 就是 的孩子。
兄弟:有相同双亲的结点互为各自的兄弟。
堂兄弟:双亲在同一层的结点互为堂兄弟。
结点的度:树中一个结点的孩子个数称为该结点的度。
树的度:树中结点的最大度数称为树的度。
分支结点/非终端结点:度大于 的结点。
叶结点/终端结点:度为 的结点。
结点的层次/深度:从树根开始定义,根结点为第 层,其孩子为第 层......以此类推。
结点的高度:以该结点为根的子树的高度。
树的高度/深度:树中结点的最大层数。
有序树:树中结点的各子树从左到右是有次序的。
无序树:不是有序树。
路径:两结点之间所经过的结点序列。
路径长度:路径上所经过的边的个数。
注意,树中的分支是有向的,从双亲指向孩子,所以树中的路径是从上向下的。同一双亲的两个孩子之间不存在路径。
森林: 棵互不相交的树的集合( 可以为 )。
正则 叉树:树中每个分支结点都有 个孩子。即树中只有度为 或 的结点。
树的结点数 等于所有结点的度数之和再加 。
度为 的树中第 层上至多有 个结点()。
高度为 的 叉树至多有 个结点。
度为 ,具有 个结点的树的最小高度为 。
证明:
度为 ,具有 个结点的树的最大高度 为 。
证明:只有一层有 个结点,其余层全只有一个结点。
注意区分于二叉树的存储结构。
根据每个结点只有一个双亲结点的性质,每个点记录其双亲结点的位置。
每个点都有一个单链表,存储其孩子。
也称作二叉树表示法。每个点只存其第一个孩子结点,以及其右兄弟的指针。
对于树:
先根遍历:先访问根结点,然后再依次遍历根节点的每棵子树。
遍历序列与这棵树对应的二叉树的先序序列相同。
后根遍历:先依次遍历根节点的每棵子树,然后再访问根结点。
遍历序列与这棵树对应的二叉树的中序序列相同。
对于森林:
二叉树是有序树(左右子树次序不能任意颠倒)。
二叉树和度为 的有序树有显著区别:
二叉树性质:
设非空二叉树中度为 的结点数为 ,叶结点(度为 的结点)的数量为 ,则有 。
证明:设度为 、、 的结点个数为 、、。结点总数 ,得到 。
几种特殊的二叉树:
满二叉树:一颗高度为 ,且有 个结点的二叉树。
可以对满二叉树按从根结点开始,自上而下,自左向右进行编号。对于编号为 的结点,如果有双亲,则其双亲为 ,如果只有左孩子,则左孩子为 ,如果有右孩子,则右孩子为 。
完全二叉树:高度为 ,有 个结点,且每个结点都与高度为 的满二叉树中编号为 的结点一一对应时。
二叉排序树:左子树上所有结点的关键字均小于(严格小于)根结点的关键字。右子树上所有结点的关键字均大于(严格大于)根结点的关键字。
平衡二叉树:树中任意一个结点的左子树和右子树的高度差的绝对值不超过 。
二叉树本身是一个逻辑结构。
完全二叉树和满二叉树比较适合顺序存储。将树上的结点存储到其编号对应的下标中。
对于一般的二叉树,也可以使用顺序结构存储。
但是最坏情况下,一个高度为 且只有 个结点的单支二叉树需要占据 个存储单元。
二叉树一般都采用链式存储结构。
含有 个结点的二叉树,使用链式存储,链表含有 个空链域。
这种存储结构也称为二叉链表。
先序/中序/后序遍历
时间复杂度:
空间复杂度:递归工作栈是遍历算法需要的辅助空间。最坏情况下,工作栈深度为树的深度,时间复杂度为
使用栈可以做到非递归:
栈中数据的含义表示后续我要访问这个点的右孩子。
后序遍历的实现稍微有些复杂,因为需要确保左右孩子都已经被访问,才能访问根结点。
这里只 GetTop(S,p) ,没有把双亲结点弹出去。当右孩子遍历完之后,会将自己弹栈,紧接着访问自己,然后设置 r,并直接令 p=NULL。这会使得下一个 while 循环直接触发 else 内容,此时栈顶结点为右孩子的双亲结点,p->rchild!=r 必然不成立,那么又会继续往上回溯。
非递归遍历时,栈中存放着当前结点以及当前结点的所有祖先结点。这可以用来求根结点到某个结点的路径、两个结点的最近公共祖先等。
层次遍历
由遍历序列构造二叉树
只给出四种遍历序列的任意一种,无法唯一确定一颗二叉树。
有两个遍历序列,且有一个中序遍历才可以唯一确定二叉树(比如只有前序+后序是不能唯一确定的)。中序遍历序列用于划分左右子树。
对二叉树进行遍历后,会得到一个遍历序列。序列中每个结点(非头尾)都有一个直接前序和一个直接后继。
一般的二叉树仅能体现出一种父子关系,不能直接得到结点在遍历序列中的前驱或后继。
二叉树使用二叉链表进行存储的时候有很多空链域,所以我们考虑利用这些空链域来记录结点的前驱和后继。这就是线索二叉树。指向结点前序和后继的指针称为线索。
线索二叉树使用如下被称为线索链表的存储结构。线索二叉树本身也是一种存储结构。但二叉树确实逻辑结构。
线索化:指将二叉树的二叉链表中的空指针改为指向前驱或者后继的线索。
中序线索二叉树
核心思想是,在对当前结点进行线索化的时候,更新当前结点的前驱和 pre 的后继。最后再把线索二叉树中的最后一个结点单独处理一下。
为了方便,可以在线索链表上添加一个头结点,lchild 指向二叉树的根节点(非线索),rchild 指向中序序列的最后一个结点(线索)。同时中序序列的第一个结点的前驱和最后一个结点的后驱都指向头结点(线索)。
求最后一个结点
也就是找最右下的结点。
将 Firstnode 函数中的 ltag 改为 rtag,lchild 改为 rchild。
求后继
如果有线索则直接用线索。没线索的话找右子树中最左下结点。
求前驱
如果有线索则直接用线索。没线索的话找左子树中最右下结点。
先序/后序线索二叉树
线索化的方法和中序差不多,就是该一下线索化和递归遍历的位置。
对于先序线索二叉树:
对于后序线索二叉树:
对于树:
对于森林:
假如树/森林 转为二叉树 , 中有 个结点,其中 个为叶结点。则 中有 个非叶结点, 中二叉链表左指针域为空的个数为 ,右指针域为空的个数为 。
结点的带权路径长度:从树的根到一个结点的路径长度与该结点上的权值的乘积。
树的带权路径长度:树中所有结点的带权路径长度之和。
哈夫曼树/最优二叉树:在含有 个带权叶结点的二叉树中,带权路径长度最小的二叉树。
构造
将所有结点视为只含有一个结点的二叉树构成的森林。然后每次,从这个森林中选择两个根节点权值最小的两个树,作为新结点的左右子树,新节点的权值为左右子树上的根节点权值之和。然后从森林中删除选出的两棵树,并把新得到的树放入森林。
性质
编码
前缀编码:没有一个编码是另一个编码的前缀。前缀编码使得解码比较简单,从左到右扫一遍就可以。
在构造出来的哈夫曼树的边上标 或 ,然后根节点到叶结点的路径上的 和 就构成了这个叶结点的哈夫曼编码。
哈夫曼树的左右分支用 表示还是 表示没有明确规定,因此构造的哈夫曼编码不唯一。
没有带路径优化得并查集,最坏情况下的时间复杂度为 。如果没有特殊说明,408 题目中会认为并查集的时间复杂度为 。
有可能只有一个结点,这样的话 Ⅲ 就不对了。
对于所有树形的情况,最少只需要 个存储单元就可以存下某个特定的树形情况。但是这个题应该是要求存储单元可以把所有树形都存下。
一个点在另一个点在左边,并不意味着这两个结点的深度是一样的。C 选项的反例是一个全右的链。
后序遍历可以做到在从 回溯的时候得到到 的路径(毕竟 的所有祖先的左右子树已经遍历完了)
根结点没有前驱,叶子节点没有后继。
度为 的哈夫曼树即为正则 叉树。度为 的点数量为 ,度为 的点数量为 。
于是有 ,得 。