二叉树
基本性质
对于所有树,结点数 == 度数和 + 1。(度只考虑出度,或者说孩子数)
定义结点层数从0开始,树深度从0开始,树高从1开始。
给定 $n$ 个结点,能构成 $h(n)$ 种不同的二叉树,结论和递推式为:
对于二叉树,度为0的结点比度为2的结点多一个
对于$n$个结点的完全二叉树,树高为$\lceil log_2(n+1) \rceil$
遍历
递归遍历
前序遍历
中序遍历
后序遍历
非递归前序遍历
思想:
使用一个栈,栈空则结束循环
每次循环,先从栈中拿出一个结点,访问它
然后把它右左结点入栈(因为栈后入先出,后放左结点,之后先出左结点)
代码:
非递归中序遍历
思想:
前序只有一种状态
中序需要用到两种状态(左子树没访问完,左子树已经访问完),分别用 bool 的 0 和 1 表示
之后的后序需要用到三种状态
每次循环,先从栈中拿出一个结点
如果左子树没访问完,则先给自己更新标志,然后左结点入栈
如果左子树已经访问完,则访问自己,然后右结点入栈
代码:
非递归后序遍历
思想:
后序需要用到三种状态(左子树没访问完,左子树已经访问完但右子树没访问完,右子树已经访问完),分别用 char 的 'n','l','r' 表示
每次循环,先从栈中拿出一个结点
如果左子树没访问完,则先给自己更新标志,然后左结点入栈
如果左子树已经访问完但右子树没访问完,则先给自己更新标志,然后右结点入栈
如果右子树已经访问完,则访问自己
代码:
另外一种方法
思想是利用两个指针,p 是当前正在处理的指针,q 记录上一个 p,以此判断右子树是否已经访问完
从前序和中序序列构建二叉树
思想:
使用递归的思想:每次先找到前序序列和后序序列中根的位置,构建左子树和右子树。构建左子树和右子树的方法如前一句话。
**代码:**给出二叉树的中序和后序遍历序列,输出层序遍历序列
另外一个例子:
线索二叉树
线索化二叉树是一种在二叉树上建立线索(thread)的方法,目的是在不增加额外空间的情况下,提高对二叉树的遍历效率。线索化的目的是使得在任意节点可以方便地找到其前驱节点(predecessor)和后继节点(successor),从而实现对二叉树的快速遍历。
假定二叉树的节点个数为n,则二叉树的空链域为n+1。想借用多余的空链域来记录某种遍历(先序遍历、中序遍历、后序遍历)的前驱与后继。根据线索的建立方式,可以分为前序线索树、中序线索树和后序线索树。
通过线索化,可以在不使用递归或栈的情况下,直接找到节点的前驱和后继节点,从而实现了对二叉树的便捷遍历。

二叉搜索树
练练手:1043 Is It a Binary Search Tree
如果全按顺序插入呢?那就变成全左子树或者全右子树的一串了,查询效率退化为 O(n)
平衡二叉树
平衡二叉树(AVL树)就是在二叉搜索树的基础上引入了平衡因子来控制树的相对平衡,不让出现全左子树或者全右子树的样子。
TODO:平衡二叉树插入,删除,ASLsucc,ASLunsucc
删除结点时,同号做单旋,异号做双旋。

如图,平均成功查找次数和平均失败查找次数为:
然而,AVL树在旋转中需要耗费时间,更适合更改少,查询多的场景。
红黑树
最长子树不超过最短子树高度的二倍即可。就是为了不让它大量的去做旋转
红黑树口诀:左根右,根叶黑,不红红,黑路同!
它通过颜色的约束来维持二叉树的平衡,它要求任意一条路径上的黑色节点数目相同,同时还需要满足一些其他特定的条件,如红色节点的父节点必须为黑色节点等。
每个节点都只能是红色或者黑色
根节点是黑色
每个叶节点(NIL 节点,空节点)是黑色的。
如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

由于红黑树的平衡度比AVL树稍低,因此在进行插入和删除操作时需要进行的旋转操作较少,但是在查找操作时效率仍然较高。红黑树适用于读写操作比较均衡的场景。
B树
在数据库查询中,以树存储数据。树有多少层,就意味着要读多少次磁盘IO。所以树的高度越矮,就意味着查询数据时,需要读 IO 的次数就越少。(众所周知,读IO 是一件费事的操作)
当数据量大的时候,用 红黑树存的话,就算是平衡树,但是也扛不住数据量大,数据量大,红黑树的树高肯定很高,那么读取数据的 IO 次数也会多。B 树的一个结点可以装多个值,读取时,是把整个结点读到内存,然后在内存中,对结点的值进行处理,在内存中处理速度肯定比磁盘快。
B树的本质是有序的多路查询树。
所以只要树的高度低,IO 少,就能够提升查询效率,这是 B 树的好处之一。B 树的缺点是:不利于范围查找(区间查找),如果要找 0~100 的索引值,那么B树需要多次从根结点开始逐个查找。
B+树
B+树和 B 树最大的不同是:B+树内部有两种结点,一种是索引结点,一种是叶子结点。B+树的索引结点并不会保存记录,只用于索引,所有的数据都保存在 B+树的叶子结点中。而B树则是所有结点都会保存数据。 B+树的叶子结点都会被连成一条链表。叶子本身按索引值的大小从小到大进行排序。即这条链表是从小到大的。因此可以直接通过遍历链表实现范围查找。
最大堆
堆是具有以下性质的完全二叉树:
每个节点的值都大于或者等于其左右孩子节点的值称为
大顶堆每个节点的值都小于或者等于其左右孩子节点的值称为
小顶堆
哈夫曼编码
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
树与森林的转换
森林的遍历分为先根遍历和后根遍历。
先根遍历与先转为二叉树再前序遍历相同。
后根遍历与先转为二叉树再中序遍历相同。
树的直径
来自PAT 甲级 1021 Deepest Root
任务:
给出N个结点与N-1条边,问:它们能否形成一棵N个结点的树?如果能,则从中选出结点作为树根,使得整棵树的高度最大。输出所有满足要求的可以作为树根的结点。
分析:
判断图是否连接用并查集解决。
当图连通时,由于题目保证只有N-1条边,因此一定能确定是一棵树,下面的任务就是选择合适的根结点,使得树的高度最大。具体做法为:
先任意选择一个结点,从该结点开始遍历整棵树,获取能达到的最深的顶点(记为结点集合A);
然后从集合A中任意一个结点出发遍历整棵树,获取能达到的最深的顶点(记为结点集合B)。
这样集合A与集合B的并集即为所求的使树高最大的根结点。
因此,这道题最快只用两次DFS就够了,比较笨的方法是找到每个叶节点然后dfs求最长路并记录,最后将所有最长路为最大值的节点输出,在PAT上也能过,但是在牛客网上是过不了的(牛客网数据略强)。
证明过程见链接
如果已知树根?递归解决
Trie前缀树
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。z
杂题思路
二叉树的最近公共祖先:递归遍历整棵二叉树,定义 表示 x 节点的子树中是否包含 p 节点或 q 节点,如果包含为 true,否则为 false。那么符合条件的最近公共祖先 x 一定满足如下条件:
完全二叉树的最后一个节点:递归,求子树的高度:如果左子树高度>右子树高度,则在左子树继续递归过程;否则在右子树继续递归。高度怎么求?由于是完全二叉树,求高度时只需一直往左遍历即可。每次递归都下降一层,每次都求树的高度。时间复杂度为O(lgN * lgN)。
完全二叉树的节点个数:先找到最大层数,找到节点个数最大和最小范围,再二分答案。
统计一个文本中的出现次数最多的k个单词(code);从数据流中找出最大的k个数
思路:HashMap(键位元素,值为出现次数)+ 维护一个只含有k个元素的优先队列。需要自定义 Comparator 按照HashMap 保存的出现次数排序,小顶堆
验证二叉搜索树,有效 二叉搜索树定义如下:
节点的左 子树 只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
思路:如何递归?
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。
思路:中序遍历
在数据流中,返回到目前为止所有元素的中位数。
思路:两个堆,实现时注意细节
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。给你一个二叉树的根节点 root ,返回其 最大路径和 。
思路:递归,递归函数计算二叉树中,以该节点为根节点的子树中寻找以该节点为起点的一条路径,使得该路径上的节点值之和最大。然后在后续处理。
最后更新于