前言
学习红黑树之前,需要有二叉搜索树、AVL树、2-3树的前置知识才可以看懂。所以现在简单介绍它们的 数据结构。
二叉搜索树(BST)
详见我之前的CSDN博客:
二叉树平衡算法一:AVL树
AVL树相关概念
-
平衡因子(BF balance factor):
BF = 左子树深度 - 右子树深度
(要注意,“深度”包不包括自身,对BF计算结果没有影响。别纠结深度的概念了,没意义)
这里不考虑绝对值,深度可以是负数。 -
最小不平衡子树:
距离插入节点最近的,且平衡因子大于1的结点为根的子树,我们称之为最小不平衡子树。如下图所示:
- 左/右旋针对谁:
只针对最小不平衡子树的那个根节点而已。
AVL树插入实现原理
原理:在逐个插入元素的过程中,保证它的平衡性。一旦发现不平衡的情况,马上处理!这样就不会造成不可收拾的情况出现。
整个过程是递归的。
步骤2:左右旋(先讲步骤2,有利于理解步骤1)
-
如果插入一个元素后,导致形成了一颗【最小不平衡子树】,那么就需要根据这颗【最小不平衡子树】的BF正负来进行左/右旋:
BF为正(其实就是BF=2):右旋最小不平衡子树的那个根节点
BF为负(其实就是BF=-2):左旋最小不平衡子树的那个根节点 -
旋转完毕后,得到的平衡子树,要与其父亲接上
-
左/右旋如果发生了占用,需要进行【断键重组】的操作。
断键重组其实很简单,只需要根据二叉搜索树性质人为判断到底接在左还是右即可(断的键如果小于当前节点,接在左边;如果大于当前节点,接在右边。一个if完事)
步骤1:左右旋前,要先进行【符号统一】
符号统一问题:若是【最小不平衡子树】与其儿子结点的BF符号不统一(正负不统一),需要先进行【符号统一】操作:
- 符号统一操作仍然需要左右旋,但是这次旋的不是【最小不平衡子树】了,而是【最小不平衡子树】的【反号子树的那个根结点】。
- 对【反号子树的那个根结点】进行左/右旋。若是【反号子树的那个根结点】BF为正,右旋;BF为负,左旋。
从而完成符号统一,然后转【步骤2】,可以继续对【最小不平衡子树】进行左/右旋。
AVL树删除实现原理
占坑,日后一定会填充(2022年年内填完)
AVL树实际代码实现(Java版)
占坑,日后一定会填充(2022年年内填完)
AVL树参考资料
本文AVL树详细案例参考了《大话数据结构》的例子:
多路查找树之2-3树
每一个结点或是具有两个孩子(2结点),或是具有三个孩子(3结点)
同时它满足排序(查找)树的性质。即元素小的在左,元素大的在右
2-3查找树实现起来比较复杂,在某些情况插入后的平衡操作可能会使得效率降低。但是2-3查找树作为一种比较重要的概念和思路对于我们后面要讲到的红黑树、B树和B+树非常重要。
但是2-3树实现起来过于复杂,所以我们介绍一种2-3树思想的简单实现:红黑树。
二叉树平衡算法二:红黑树
必须学会了AVL树和2-3树,你才能看懂红黑树,否则想都别想。
红黑树的本质:以二叉树的形式,实现2-3树。(即”二维化“的2-3树)
红黑树和左偏红黑树
红黑树家族有很多实现形式,这里只介绍其中一种:左偏红黑树。
左偏红黑树出自《算法》(第4版)。有其具体代码实现逻辑。
红黑树相关概念定义
不同红黑树的定义是不一样的,这里给出的是左偏红黑树的定义:
- 1、红连接均为左连接(红色结点总为左子树身上的结点),红色连接指的结点为红色结点
- 2、没有任何一个结点同时和两条红链接相连;(不存在两个连续的红色结点)
- 3、该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同;
【空链接】指的是null连接,如下图所示:
红黑树插入实现原理
插入时候要保证两条规律:
- 规律1:凡是插入的结点必是红色结点;不管是插入到左子树还是插入到右子树。
- 规律2:根节点的颜色一定是黑色的!
同时还不能破坏上面说的【红黑树相关概念定义】
原则:在逐个插入元素的过程中,保证红黑树的特性和规律。一旦发现情况不对,马上处理!这样就不会造成不可收拾的情况出现。
下面直接讲原理!
左右旋
左/右旋针对谁:只针对 最小不符合红黑树性质子树的那个根节点而已。
- 【插入新的结点出现红色右连接且该结点左连接为黑色】左旋:X结点左黑右红,左旋转X结点:
- 【插入新的结点出现连续两个红色左连接】右旋:
右旋可能会导致出现红色右连接:即为:
这种情况后续会通过【颜色反转】解决该问题
下面两条注意事项同AVL树一模一样:
- 旋转完毕后,得到的符合红黑树定义子树,要与其父亲接上
- 左/右旋如果发生了占用,需要进行【断键重组】的操作。
断键重组其实很简单,只需要根据二叉搜索树性质人为判断到底接在左还是右即可(断的键如果小于当前节点,接在左边;如果大于当前节点,接在右边。一个if完事)
颜色反转
当一个左子树和右子树的颜色都是红色:
这就是出现了多路查找树(2-3-4树)的 4- 结点情况。因为2-3树的3- 结点其实就是红节点和父亲组成。那么有两个红结点和其父亲,那不就组成了一个4-结点了。
这种情况下只需要进行颜色反转:左右子树变成黑,同时左右子树的父亲变红即可:
注意:若是颜色反转出现在Head(头结点),为了保证二叉树的性质(整个红黑树根结点一定是黑色的),那就变成三个节点全黑!!
红黑树删除实现原理
占坑,日后一定会填充(2022年年内填完)
红黑树实际代码实现(Java版)
已经实现了除了红黑树删除的全部算法。红黑树删除待填充:
package com.daji.base_data_structure.Tree.red_black_tree;
/*
* 左偏型红黑树的代码实现
* 参考资料:黑马数据结构 —— 红黑树
* 已经全部实现,后续只需要复习即可。
* TODO 这个类是严格面向对象设计的,非常严谨。如果看不懂去看LinkList.java这个类,一样的设计方式。
* FIXME 没有提供删除的API。删除的实现较为复杂,待日后填充吧
*/
public class Left_RedBlackTree<Key extends Comparable<Key>, Value> {
//根节点
//TODO 这个类是严格面向对象设计的,非常严谨。如果看不懂去看LinkList.java这个类,一样的设计方式。
private Node root;
//记录树中元素的个数
private int SIZE;
//红色链接
private static final boolean RED = true;
//黑色链接
private static final boolean BLACK = false;
//结点类
private class Node {
//存储键
public Key key;
//存储值
private Value value;
//记录左子结点
public Node left;
//记录右子结点
public Node right;
//由其父结点指向它的链接的颜色
public boolean color;
public Node(Key key, Value value, Node left, Node right, boolean color) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.color = color;
}
}
//获取树中元素的个数
public int size() {
return SIZE;
}
/**
* 判断当前节点是否为红色结点
*
* @param x
* @return
*/
private boolean isRed(Node x) {
if (x==null){
return false;
}
//如果是红色返回TRUE
return x.color==RED;
}
/**
* 左旋转(右红即左旋)
* 当出现本结点的右连接是红色的情况(X结点左黑右红,左旋转X结点)
* 【大吉】已经覆写完毕
* @param node
* @return
*/
private Node rotateLeft(Node node) {
Node RIGHT = node.right; //RIGHT:该结点的右结点
//“断键重组”操作,RIGHT的左结点断裂,直接赋值给node的右结点即可
node.right = RIGHT.left;
//左旋动作
RIGHT.left = node;
//颜色根据链条的指向进行重组
RIGHT.color = node.color; //让RIGHT结点的color属性变为原结点的color属性
node.color = RED; //新加入的结点总是红色的
return RIGHT;
}
/**
* 右旋(连续左红即右旋)
* 当出现本结点的左儿子和左左儿子全部都是红色情形(连续红节点),右旋该结点
* 【大吉】已经覆写完毕
*
* @param node
* @return
*/
private Node rotateRight(Node node) {
Node LEFT = node.left;//找到该节点的左结点
//“断键重组”操作,LEFT的右结点断裂,直接赋值给node的右结点即可
node.left = LEFT.right;
//右旋动作
LEFT.right = node;
//颜色根据链条的指向进行重组
LEFT.color = node.color; //让LEFT结点的color属性变为原结点的color属性
node.color = RED; //新加入的结点总是红色的
return LEFT;
}
/**
* 颜色反转,相当于完成拆分4-节点
* 当一个结点的左右子树的颜色全部都是RED,也就是出现了临时的4结点,只需要把这三个结点进行颜色反转即可。
* 【注意】如果颜色反转出现在整棵树的顶点头结点,那么就全部置为黑色结点(三个黑)
* 【大吉】已经覆写完毕
*
* @param node
*/
private void flipColors(Node node) {
//TODO 判断是否入参是整个树的顶点。黑马版本暂时无法判断,所以写个空,以后有机会实现
/*
if (node == head){
//TODO 全部变成黑色结点
node.color = BLACK;
node.left.color=BLACK;
node.right.color = BLACK;
}
*/
//当前结点变为红色
node.color = RED;
//左子结点和右子结点变为黑色
node.left.color=BLACK;
node.right.color = BLACK;
}
/**
* 在整个树上完成插入操作
* 和底下的方法的区别:由于这颗红黑树的root结点并不是第一个结点,root
*
* @param key
* @param val
*/
public void put(Key key, Value val) {
root = put(root,key,val);
//根结点的颜色总是黑色。这一步其实没错,root永永远远就是根结点。不信看重载方法的返回结果就行了。
root.color = BLACK;
}
/**
* 在指定树中,完成插入操作,并返回添加元素后新的树
* 注意,这是个私有方法
* 运用了非常巧妙的递归操作,认真看。
* 较难实现
*
* @param node
* @param key
* @param val
*/
private Node put(Node node, Key key, Value val) {
//判断node是否为空,如果为空则直接返回一个红色的结点就可以了(新插入的结点必红)
if (node == null){
//数量+1
SIZE++;
return new Node(key,val,null,null,RED);
}
//比较node结点的键和key的大小
int cmp = key.compareTo(node.key);
if (cmp<0){
//继续递归往左
node.left = put(node.left,key,val);
}else if (cmp>0){
//继续递归往右
node.right = put(node.right,key,val);
}else{
//发生值的替换(这里其实是仿照map的设计,如果map有相同的key,那么就替换value)
//如果不设计成map的API,只管插入Integer,就没有这个必要了
//不是红黑树的实现重点。只是黑马仿照了map设计而已
node.value = val;
}
//【重点】从这里往下其实都是递归执行了,包括上面也是递归的。所以该方法可以实现递归旋转整棵树
//【重点】从这里往下其实都是递归执行了,包括上面也是递归的。所以该方法可以实现递归旋转整棵树
//【重点】从这里往下其实都是递归执行了,包括上面也是递归的。所以该方法可以实现递归旋转整棵树
//【重点】从这里往下其实都是递归执行了,包括上面也是递归的。所以该方法可以实现递归旋转整棵树
//颜色反转:当前结点的左子结点和右子结点都为红色时,需要颜色反转
//优先判断颜色翻转情形。
//注意特殊情形:如果出现X左右都是红色情形,那么无需任何旋转,直接颜色反转即可(该特殊情形为大吉总结,黑马版并没有写)
if (isRed(node.left) && isRed(node.right)){
flipColors(node);
}
//进行左旋:当当前结点node的左子结点为黑色,右子结点为红色,需要左旋
if (isRed(node.right) && !isRed(node.left)){
node = rotateLeft(node);
}
//进行右旋:当当前结点node的左子结点和左子结点的左子结点都为红色,需要右旋
//这里的判断条件只能这样写,如果&&符号的两侧条件翻转,就会导致空指针的发生!
//这里的判断条件只能这样写,如果&&符号的两侧条件翻转,就会导致空指针的发生!
//这里的判断条件只能这样写,如果&&符号的两侧条件翻转,就会导致空指针的发生!
if (isRed(node.left) && isRed(node.left.left)){
node = rotateRight(node);
}
/*
这个return,可能经过各种旋转操作改变了"根(root)"的结点位置
但是其对于调用者来说,其永远就代表着"根(root)"结点。
好好体会这里递归的妙处。
*/
return node;
}
//根据key,从树中找出对应的值
//很简单。就是调用二分查找嘛
public Value get(Key key) {
return get(root,key);
}
//从指定的树x中,查找key对应的值
//很简单。就是递归二分查找嘛,没什么好说的了
public Value get(Node x, Key key) {
if (x == null){
return null;
}
//比较x结点的键和key的大小
int cmp = key.compareTo(x.key);
if (cmp<0){
return get(x.left,key);
}else if (cmp>0){
return get(x.right,key);
}else{
return x.value;
}
}
}
AVL树参考资料
黑马程序员《数据结构与算法》Java版本:
https://www.bilibili.com/video/BV1iJ411E7xW
二叉树平衡:AVL和红黑树的联系区别
联系
- 都是二叉排序树(二叉搜索树)的平衡算法,只不过是不同的实现形式罢了。
区别
- AVL树是严格平衡的二叉树,要求每个结点的左右子树高度差不超过1(BF严格位于-1,0,1之间);而红黑树要宽松一些,要求任何一条路径的长度不超过其他路径长度的两倍
- AVL树的查找效率比红黑树更高(因为平衡更加严格),但是AVL树平衡调整的成本也更高。
- 所以,在需要频繁查找时,选用AVL树,需要频繁插入删除,使用红黑树结构。
多路查找树之B树、B+树(参考黑马程序员)
B树
B树的插入要有一个升阶的操作。很好理解
阶数一般是大于100的,在特定场景下,查询效率特别高!
B树在IO中的应用
应用场景:内存和外存之间的交换,内外存的查找性能更多取决于内外存之间读取的次数,所以读写次数越少,效率越高。B树的阶数在非常大的情况下,只需要很少的内外存交换就可以完成查找!
内存存放B树的根节点,其左右链接指向外存;外存由于是分页存储的,让B树的阶数和硬盘存储的页面大小相匹配,那么内存只需要访问一两次访问外存的页面,就可以索引到想要的数据。这样最大限度减少了读写次数。
B+树
B+树是对B树的一种变形树,它与B树的差异在于:
- 非叶结点仅具有索引作用,也就是说,非叶子结点只存储key,不存储value;
- 树的所有叶结点构成一个有序链表,可以按照key排序的次序遍历全部数据。
怎么理解呢?看下图就知道了:
叶子节点如上图所示,是链表结构链接。
B+树和B树的对比:
B+ 树的优点在于:
1.由于B+树在非叶子结点上不包含真正的数据,只当做索引使用,因此在内存相同的情况下,能够存放更多的key。
2.B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。
B树的优点在于:
由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。
B+树在数据库索引中的应用
总结时间复杂度
AVL树时间复杂度:O(logn)
查找、插入、删除均为O(logn)
普通的二叉排序树
最好情况就是严格平衡,也就是AVL情形,O(logn)
最坏情况退化成链表,就是O(n)
红黑树 O(logN)
同AVL树。