树可以说是某些人最喜欢的数据结构。
二叉树的概念
在计算机领域,二叉树是每个节点最多有两个子树的结构。通常子树被称为左子树和右子树。
二叉树的特例:
满二叉树
满二叉树是完全二叉树的特例。
- 所有叶节点必须在同一层上
- 除了叶子节点的所有节点都有两个子节点
完全二叉树
完全二叉树可以看成是满二叉树的最后一行右侧部分连续缺失。(最大堆和最小堆就是完全二叉树)
平衡二叉树
对于任何一个节点,左树和右树的绝对值差不超过1
二叉树遍历
二叉树的遍历主要有深度优先和广度优先两种。深度优先又包含前序遍历,中序遍历和后序遍历。个有个的应用场景
以如下例子说明
前序遍历
首先遍历根节点,然后是根节点的左侧节点,然后继续遍历左侧节点的左侧节点,…一直到最左边的叶节点。然后后退一层,访问右节点,然后继续后退一层。
简单来说就是先根节点,然后左节点,最后右节点。
A B C D E F G
除了采用递归的办法,前序遍历还可以使用栈实现。
- 访问结点P,并将结点P入栈;
- 判断结点P的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点P,循环至1);若不为空,则将P的左孩子置为当前的结点P;
- 直到P为NULL并且栈为空,则遍历结束。
中序遍历
从根节点开始找,如果根节点有左侧节点,就将寻找的指针向左侧移动。
首先是左节点,然后是根节点,最后是右节点
其实反过来更好理解。如果有下列两者之一情况,任何一个右节点都不能被遍历
- 对应父节点没有被遍历
- 对应的左节点没有被遍历
C B D A E G F
注意,如果有多个节点符合被遍历的条件,离最后一个遍历点最近的点先被遍历。
非递归实现方法如下
- 对于节点P,若其左孩子不为空,则将P入栈并将P的左孩子置为当前的P,然后对当前结点P再进行相同的处理;
- 若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的P置为栈顶结点的右孩子;
- 直到P为NULL并且栈为空则遍历结束。
二叉查找树的中序遍历就是一个递增序列
后序遍历
首先是左节点,然后是右节点,最后是根节点
和上面的道理类似。
C D B G F E A
后续遍历的非递归实现是最复杂的。
对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问, 因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就 保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是 否是第一次出现在栈顶。
后序遍历保证在操作某个节点时,肯定已经操作过其两个子节点,可以用于二叉树的节点删除
广度优先遍历
广度优先遍历也被成为层次遍历,使用队列实现。
- 从队列头取出节点P,访问节点P,如果其有子节点,将左节点和右节点先后入队。
广度优先遍历常被用于序列化二叉树,优点是不需要读取完整序列就可以开始重构过程。
树的旋转
E节点左旋。常见的例子比如说要在下面的S的左子节点下添加一个新节点作为左子节点的子节点,就会导致下图的二叉树失去平衡,左旋以下就可以解决。
类似的我们有右旋
B-树
首先明确一点,B-树不读做B减树。中间的是连字符。这个B代表平衡的意思,但是B-树不是平衡二叉树。
B-tree被成为多路查找树,相对于二叉树,B-tree更加矮胖。这样做的原因是因为物理实现磁盘IO限制,矮胖有助于减少磁盘读取索引的io次数。
B+树
B+树在B-树的基础上,限制所有数据必须存储在叶节点,这样之后,就可以为叶节点添加一个链表。这让我门按照索引进行快速范围读取更加容易。
- mysql InnoDB引擎使用B+树作为默认索引
B*树
在非根非叶节点增加了只想相邻兄弟节点的指针。
这是为了树的分裂的需要。当一个节点满后,B+树会访问自己的父节点,然后在父节点上新建一个与自己相邻子节点,最后将自己的一半数据转移到新节点上。
B*树则不会优先创在新节点。利用自己指向兄弟节点的指针,他会首先尝试将自己的数据转移到自己的兄弟节点上。于是,他对树空间利用率较高。
- B*是oracle中最长用到的索引
二叉搜索树
(1)若左子树非空,则左子树的所有节点小于他的根节点
(2)若右子树非空,则右子树的所有节点大于他的根节点
(3)左右子树也都是二叉排序树
显然排序二叉树可以用作排序,也可以用作快速查找和插入。一般来讲排序算法的时间复杂度为Nlog2N
查找算法的时间复杂度为log2N
最大堆和最小堆
最大堆最小堆是完全二叉树,所以底层可以采用数组实现。
最大堆和最小堆常用语实现优先队列。
最大堆和最小堆用于堆排序。
插入一个数据,就是把这个数据放在最底层的最后一个(也就是数组末尾),然后判断能不能上浮。也就是和它的父节点比较。他的父节点的位置就在他的数组坐标的1/2处。判断到后直接交换即可。
AVL树
自平衡二叉查找树。查找、插入和删除在平均和最坏情况下都是O(logn)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。
红黑树(RBTree)
红黑树放弃追求绝对的平衡。因为放弃了追求绝对平衡,所以查找效率相对较低。但是,在处理插入带来的失衡的需要进行rebalance上,展现了优点,最多只需要三次旋转就能恢复平衡,时间复杂度为o(1),而AVL树则需要logN
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
- 节点是红色或黑色。
- 根节点是黑色。
- 每个叶节点(NIL节点,空节点)是黑色的。
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
适用于数据量小的需要排序的数据集(树的高度比较高,数据量过大需要频繁读取磁盘)
- linux内部epoll的实现
- java中的TreeMap和TreeSet
AVL树和RBT的比较
如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。
其次,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。
map的实现只是折衷了两者在search、insert以及delete下的效率。总体来说,RB-tree的统计性能是高于AVL的。
LSM树
日志结构的合并树(LSM-tree)是一种基于硬盘的数据结构,与B-tree相比,能显著地减少硬盘磁盘臂的开销,并能在较长的时间提供对文件的高速插入(删除)。然而LSM-tree在某些情况下,特别是在查询需要快速响应时性能不佳。通常LSM-tree适用于索引插入比检索更频繁的应用系统。
HBase和RocksDB都采用了这种实际,在牺牲读性能的前提下增加了写入性能(一旦读操作不命中内存就会导致对内存的读写)。
LSM树原理把一棵大树拆分成N棵小树,它首先写入内存中,随着小树越来越大,内存中的小树会flush到磁盘中,磁盘中的树定期可以做merge操作,合并成一棵大树,以优化读性能。
总结
- 一般数据存储引擎有三种:
- Hash存储引擎
- B-/B+/B*树
- LSM树