没有必要过度关注本文中二叉树的增删改导致的结构改变,规则操作什么的了解一下就好,看不下去就跳过,本文过多的XX树操作图片纯粹是为了作为规则记录,该文章主要目的是增强下个人对各种常用XX树的设计及缘由的了解,也从中了解到常用的实现案例使用XX树实现的原因。
数据在计算机中的存储结构主要为顺序存储结构、链式存储结构、索引存储结构、散列存储结构,其中链式存储结构最常见的示例是链表与树,链式存储结构主要有以下特点:
度:当前节点下的子节点个数
二叉树是每个节点最多有两个子树的树结构,左侧子树节点称为“左子树”(left subtree),右侧子树节点称为“右子树”(right subtree)。每个节点最多有2个子节点的树(即每个定点的度小于3)。
二叉树的特点
除了叶子节点外每一个节点都有两个子节点,且所有叶子节点都在二叉树的同一高度上。
如果二叉树中除去底层节点后为满二叉树,且底层节点依次从左到右分布,则此二叉树被称为完全二叉树。
二叉查找树根节点的值大于其左子树中任意一个节点的值,小于其右子树中任意一节点的值,且该规则适用于树中的每一个节点。
二叉查找树的查询效率介于O(log n)~O(n)之间,理想的排序情况下查询效率为O(log n),极端情况下BST就是一个链表结构(如下图),此时元素查找的效率相等于链表查询O(n)。
二叉查找树需要注意的是删除节点操作时的不同情况,删除节点根据节点位置会有以下三种情况:
为什么不用右子树中的最小叶子节点值取代删除节点?个人认为是为了维持范围值(纯属臆测):
右子树中的最小叶子节点值大于删除节点左子树中的所有节点,但若该叶子节点比删除节点大很多,这将会大大扩大左子树的范围值,左子树可插入的范围值也会大大增大,对左子树的查询效率造成较大的影响 左子树中的最大叶子节点值也大于删除节点左子树中其它所有的节点,虽然是使用该节点替代删除节点会缩小的左子树的值范围,但也减少左子树的插入范围值,对左子树的查询影响不大
由上可以看出,二叉查找树(BST)无法根据节点的结构改变(添加或删除)动态平衡树的排序结构,也因此对某些操作的效率造成一定的影响,而AVL树在BST的结构特点基础上添加了旋转平衡功能解决了这些问题。
AVL树是最早被发明的自平衡二叉搜索树,树中任一节点的两个子树的高度差最大为1,所以它也被称为高度平衡树,其查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log n)。
平衡二叉搜索树由Adelson-Velskii和Landis在1962年提出,因此又被命名为AVL树。平衡因子(平衡系数)是AVL树用于旋转平衡的判断因子,某节点的左子树与右子树的高度(深度)差值即为该节点的平衡因子。
AVL树的特点
为什么选择AVL树而不是BST?
大多数BST操作(如搜索、最大值、最小值、插入、删除等)的时间复杂度为O(h),其中h是BST的高度。对于极端情况下的二叉树,这些操作的成本可能变为O(n)。如果确保每次插入和删除后树的高度都保持O(log n),则可以保证所有这些操作的效率都是O(log n)。
节点插入、旋转
AVL树插入节点的如下:
旋转的方式:
LL - 插入节点是失衡节点u左子节点ul上的左子树节点
gif图中的高度是从叶子节点开始计算的,因为插入节点后是从下往上检测节点的平衡因子,所以叶子节点高度恒为1更方便平衡因子的运算
LR - 插入节点是失衡节点u左子节点ul上的右子树节点
RR - 插入节点是失衡节点u右子节点ur上的右子树节点
RL - 插入节点是失衡节点u右子节点ur上的左子树节点
规律总结:
u u uls
/ \ / \ / \ ul T3 Left Rotate (us) uls T3 Right Rotate(u) ul u / \ - - - - - - - - -> / \ - - - - - - - -> / / \
T1 uls ul T2 T1 T2 T3
\ /
T2 T1 复制代码
节点删除步骤
例子:
AVL树伪代码
AVLTree{
private Node root; private class Node{
// height从叶子节点开始计算(即叶子节点恒为1,方便遍历父节点的平衡因子计算)
int val,height;
Node left; Node right; public Node(int val){ height = 1;
this.val = val; } } public void insert(int val){ if(root == null){
root = new Node(val); } else {
insert(root, val); } } /** * 将值val按AVL规则插入到节点树node下 * 1. 根据BST规则遍历插入val节点,更新插入经过的路径上的节点高度
* 2. 检测是否有失衡节点,没有失衡则直接设置高度,失衡则旋转再调整高度
* @param node 插入节点到node子树 * @param val 插入的节点值 */
private insert(Node node, Integer val);
/**
* 获取节点的失衡因子:left.height - right.height */
int getBalance(Node node);
/**
* 节点左旋,调整旋转的节点高度height */
leftRotate(Node node);
/**
* 节点右旋,调整旋转的节点高度height */
rightRotate(Node node);
}
红黑树是一种自平衡二叉搜索树(BST),且红黑树节点遵循以下规则:
相比AVL树
AVL树比红黑树更加平衡,但AVL树可能在插入和删除过程中引起更多旋转。因此,如果应用程序涉及许多频繁的插入和删除操作,则应首选Red Black树(如 Java 1.8中的HashMap)。如果插入和删除操作的频率较低,而搜索操作的频率较高,则AVL树应优先于红黑树。
个人引申的疑问
原因可从后续的插入步骤与演示案例得出
插入节点
红黑树插入节点后违反的主要规则是两个连续的红色节点。
插入步骤:
演示案例
引申疑问自答
以上也是Java 8的HashMap中树节点实现结构采用红黑树而不是AVL树的原因
删除节点
删除节点主要违反的规则是子树中黑色高度的更改,导致根节点到叶子路径的黑色高度降低。
红黑树删除时一个比较复杂的过程,为了更容易的理解删除过程,可以使用双黑概念去简化理解该过程。 双黑概念指当删除黑色节点后使用了另一个黑色节点替代删除节点的位置(也可以当成有两个null的黑色叶子节点因删除重叠成1个),这也意味着根节点到替代节点的原路径上少了一个黑色节点导致违反了到任一叶子节点路径上含相同的黑色节点数的节点规则(黑色)。当删除时出现双黑情况,则需要通过旋转将节点转换为单黑色(重叠的两个黑色null节点重新铺展为2个)。
删除步骤
大多数自平衡搜索树(如AVL和红黑树)都会假定所有数据都在主内存中,但我们必须考虑无法容纳在主内存中的大量数据。当键的数量很大时,将以块形式从磁盘读取数据,与主存储器访问时间相比,磁盘访问时间非常高。 B树是一种自平衡搜索树,设计的主要思想是减少磁盘访问次数。大多数树操作(增、删、查、最大值、最小值等)都需要都需要O(h)磁盘访问,h为树的高度。B树通过在节点中放置最大可能的键来保持B树的高度较低。通常,B树节点的大小保持与磁盘块大小相等。由于B树的高度较低,因此与平衡的二叉搜索树(如AVL树、红黑树等)相比,大多数操作的磁盘访问次数显著减少。
磁盘块是一个虚拟的概念, 是操作系统(软件)中最小的逻辑存储单位,操作系统与磁盘打交道的最小单位是磁盘块。
一颗m阶(m指一个节点中最多包含的子节点数)B树特点如下:
搜索
B-树搜索类似于搜索二叉树,算法与递归算法相似。在B树中,搜索过程也是从根节点开始,通过与节点key值比较进行搜索,搜索操作的时间复杂度为O(log n)。具体的搜索步骤如下:
插入
设B树的阶为m,则插入流程如下:
4阶B树插入示例
以下示例皆为4阶B树(m=4),则有以下规则:
插入流程 2.1 & 2.2.b 示例
插入流程2.2.a 示例
删除
B树的删除比插入要复杂得多,因为我们可以从任何节点(不仅是叶子)中删除key,而且从内部节点删除key时,我们将不得不重新排列节点的子节点。 从B树中删除键的各种情况(设删除键k所在节点为n):
实现动态多级索引时,通常会采用B树和B+树的数据结构。但是,B树有一个缺点是它将与特定键值对应的数据指针(指向包含键值的磁盘文件块的指针)以及该键值存储在B树的节点中。该设计大大减少了可压缩到B树节点中的条目数,从而增加了B树中级别数与记录的搜索时间。 B+树通过仅在树的叶节点处存储数据指针来消除上述B树的缺点,因而B+树的叶节点结构与B树的内部节点结构完全不同。数据指针在B+树中仅存在于叶节点,因此叶节点必须将所有键值及其对应的数据指针存储到磁盘文件块以便访问。此外,叶节点也用于链接以提供对记录的有序访问。因此,叶节点才是第一级索引,而内部节点只是索引到其它级别索引的多层索引。叶节点的一些键值也出现在内部节点中,主要是作为简化搜索记录的一种媒介。 B+树与具有同级的B树相比,具有同级的B+树可以在其内部节点中存储更多键,显着改善对任何给定关键字的搜索时间,同样的键数B+树级别较低且含指向下一个节点的指针P的存在使B+树在从磁盘访问记录时非常快速有效。如设B树与B+树某一级别内部节点都有100K的容量,由于B树的节点除了存键和数据指针,所以实际存的键容量连一半50K可能都没有,但B+树的100K容量都用于存键了,所以索引自然更高效。
B树与B+树的区别:
B树 B+树 所有节点都有数据指针 数据指针集中在叶节点 叶节点不存储为链表结构 叶节点存储为链表结构 并非所有键都在叶节点上,搜索需花费更多时间(重复中序遍历) 所有键都在叶节点上,搜索更快更准确(根据key找到大致叶节点后基于叶节点的链表查询) 树中不会有重复键 键重复出现,且所有key、数据节点都在叶子上 没有多余的搜索键 可能存在冗余搜索键 内部节点的删除非常复杂,并且树必须进行大量转换 删除任何节点都很容易,因为所有节点都可以在叶子上找到 插入会花费更多时间,有时无法预测 插入更容易,结果始终相同
数据结构 实现案例 理由 红黑树 JDK 1.8的HashMap HashMap的增删操作多,相比AVL树使用红黑树实现可以减少树的旋转操作 B-Tree MongoDB索引 1. 普通的二叉树或平衡树无法支撑数据库的大数据量(参考B-Tree简介)
2. MongoDB是非关系型聚合数据库,B树恰好将键字段和数据字段聚合在一起,而B+树中的内部节点不存储数据,叶节点间链表连接的优势在MongoDB的JSON数据格式面前也不明显
3. B树所有节点都有数据指针,MongoDB存Mongodb使用B树只要找到指定的索引,就可进行数据访问,避免了叶节点的访问。 B+Tree MySQL索引 关系型数据库最常用的是数据遍历与范围操作,基于B-Tree的设计理由与B-Tree的缺点,B+树所有数据都存储在叶节点中,并且通过指针串在一起,因此很容易进行间隔遍历甚至或遍历
B-Tree缘由:大多数自平衡搜索树(如AVL和红黑树)都会假定所有数据都在主内存中,但我们必须考虑无法容纳在主内存中的大量数据。当键的数量很大时,将以块形式从磁盘读取数据,与主存储器访问时间相比,磁盘访问时间非常高。
搞图搞得最多最耗时的一次笔记如有错误,还请指出。
作者:WilsonHe
链接:https://juejin.im/post/5efc04c7e51d45347c1b6efa
来源:掘金