数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)

平衡树

当我们依次往二叉查找树中插入9,8,7,6,5,4,3,2,1这个九个数据时,最终构出来的树是个非平衡树,如下:
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第1张图片
当要查找1这个元素时,查找效率会很低,原因在于这个树它不平衡,全部都是向左边的分支。如果让生成的树像完全二叉树那样,查找效率更高。常见的平衡树有如下几个2-3查找树,红黑树,B树和B+树等。。。

2-3查找树

定义

2-3树是最简单的B树结构。
如图
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第2张图片
特点:
(1)2-3树的所有叶子结点都在同一层
(2)有两个子节点的结点叫结点,二节点要么没有子结点,要么有两个子结点
(3)有三个子结点的结点叫三结点,三结点要么没有子结点,要么有三个子结点
(4)2-3树是由二结点和三结点构成的树

插入规则
(1)2-3树的所有叶子结点都在同一层。
(2)二结点要么没有结点,要么有两个子结点,且这两个结点中的左结点小于右结点;三结点要么没结点,要么有三个子结点,且从左往右结点大小增大。
(3)当按照规则插入一个数到某个结点时,不能满足上述两个要求的就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面3个条件
(4)对于三结点的子树的值大小仍然遵守BST二叉排序树的规则。

查找

将二叉查找树的查找算法一般化我们就能够直接得到2-3树的查找算法。要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较的结果找到指向相应区间的连接,并在其指向的子树中递归地继续查找。如果这个是空链接,查找未命中。
如:查找H
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第3张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第4张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第5张图片

插入

往2-3树中插入元素和往二叉查找树中插入元素一样,首先要进行查找,然后将结点挂到未找到的结点上。2-3树之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。如果查找后未找到的结点是一个2-结点,那么很容易,我们只需要将新的元素放到这个2-结点里面使其变成一个3-结点即可。但是如果查找的结点点结束于一个3-结点,那么可能有点麻烦。
插入2-结点中

数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第6张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第7张图片
向一个只含有一个3-结点的树中插入新键
假设2-3树只包含一个3-结点,这个结点有两个键,没有空间来插入第三个键了,最自然的方式是我们假设这个结点能存放三个元素,暂时使其变成一个4-结点,同时他包含四条链接。然后,我们将这个4-结点的中间元素提升,左边的键作为其左子结点,右边的键作为其右子结点。插入完成,变为平衡2-3查找树,树的高度从0变为1。
例如:
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第8张图片
向一个父结点为2-结点的3-结点中插入新键
和上面的情况一样一样,我们也可以将新的元素插入到3-结点中,使其成为一个临时的4-结点,然后,将该结点中的中间元素提升到父结点即2-结点中,使其父结点成为一个3·结点,然后将左右结点分别挂在这个3-结点的恰当位置。
如下:
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第9张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第10张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第11张图片
向一个父结点为3-结点的3-结点中插入新键
当我们插入的结点是3-结点的时候,我们将该结点拆分,中间元素提升至父结点,但是此时父结点是一个3-结点,插入之后,父结点变成了4-结点,然后继续将中间元素提升至其父结点,直至遇到一个父结点是2-结点,然后将其变为3-结点,不需要继续进行拆分。
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第12张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第13张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第14张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第15张图片
分解根结点
当插入结点到根结点的路径上全部是3-结点的时候,最终我们的根结点会编程一个临时的4-结点,此时,就需要将根结点拆分为两个2-结点,树的高度加1。
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第16张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第17张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第18张图片
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第19张图片

2-3树的性质

通过对2-3树插入操作的分析,我们发现在插入的时候,2-3树需要做一些局部的变换来保持2-3树的平衡。—棵完全平衡的2-3树具有以下性质︰

  1. 任意空链接到根结点的路径长度都是相等的
  2. 4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时,树高+1.
  3. 2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向上生长。

红黑树

可以看到,2-3树的操作比较复杂,虽然它能保证在插入元素之后,树依旧保持平衡状态,但实现起来比较复杂。因此,这里有一种以2-3树思想为基础的简单实现:红黑树。
红黑树主要是对2-3树进行编码,基本思想是用标准的二叉查找树和一些额外的信息(替换3-结点)来表示2-3树,通常将树中的链接分为两种类型:
红链接:将两个2-结点链接起来构成一个3-结点;
黑链接:2-3树中的普通链接。
或者这么说吧:
将3-结点表示为有一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。这样表示就相当于在使用二叉查找树了,就简化了操作。数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第20张图片

红黑树定义

红黑树是含有红黑链接并满足下列条件的二叉查找树:
1、红链接均为左链接
2、没有任何一个结点同时和两条红链接相连
3、红黑树是完美黑色平衡的(即任意空链接到根结点上的路径上的黑链接数量相同)
如图:
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第21张图片
相当于如下图
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第22张图片
当把红连接断掉,并把红链接连在一起的结点合并在一起就形成了3-结点,如下:
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第23张图片

红黑树的平衡化操作

在读红黑树进行增删改查的操作后,很可能会出现红色的右链接或者两条连续红色的连接,而这些都不满足红黑树的定义,所以要通过旋转进行修复,让红黑树保持平衡。

左旋

当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。

前提:当前结点为h,它的右子结点为x;
左旋过程:
1、让x的左子结点变为h的右子结点:h.right = x.left;
2、让h成为x的左子结点:x.left=h;
3、让h的color属性变为x的color属性值:x.color = h.color;
4、让h的color属性变为RED:h.color = true;

数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第24张图片

右旋

当某个结点的左子结点是红色,且左子结点的左子结点也是红色的情况下需要右旋
前提:当前结点为h,它的左子结点为x
右旋过程:
1、让x的左子结点称为h的左子结点:h.left = x.right
2、让h成为x的右子结点:x.right = h
3、x的color变为h的color属性值:x.color = h.color
4、让h的color为RED;
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第25张图片

但此时你会发现,右旋后的树并不满足红黑树定义,因为结点x的两条链接都是红色,所以还需要进行颜色反转的操作,具体请往下看

红黑树的插入

向单个2-结点中插入新键

一棵只含有一个键的红黑树只含有一个2-结点,插入另一个键后,需要立马将它们旋转,使之满足红黑树的定义

  1. 如果插入的新键小于当前结点的键,只需要新增一个红色结点即可,新的红黑树和单个3-结点完全等价
    数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第26张图片

  2. 如果新键大于当前结点的键,那么新增的红色结点将会产生一条红色的右链接,此时我们需要通过左旋,把红色右链接变成左链接,插入操作才算完成。形成的新的红黑树依然和3-结点等价,其中含有两个键,一条为红色链接
    数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第27张图片

向底部的2-结点插入新键

用和二叉查找树相同的方式向一棵红黑树中插入一个新键,会在树的底部新增一个结点(可以保证有序性),唯一区别的地方是会用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种方式仍然适用。

数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第28张图片

颜色反转

当一个结点的左子结点和右子结点的color都为RED(红色)时,就出现了临时的4-结点,此时只需要把左子结点和右子结点的颜色变为BLACK,同时让当前结点的颜色变为RED即可

数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第29张图片

向一棵双键树(即一个3-结点)中插入新键

情况一:新键大于原树中的两个键
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第30张图片
情况二:新键小于原树中的两个键

数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第31张图片
还需要颜色反转
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第32张图片
情况三:新键介于原树中两个键之间

数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第33张图片
先左旋
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第34张图片
再右旋并进行颜色反转
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第35张图片

需要注意:由于根结点不存在父结点,且结点对象中的color属性表示的是父结点指向当前结点的连接的颜色,所以每次进行插入操作后,都需要将根结点的颜色设置为黑色

向树底部的3-结点插入新键

假设在树的底部的一个3-结点下加入一个新的结点。前面我们所讲的3种情况都会出现。指向新结点的链接可能是3-结点的右链接(此时我们只需要转换颜色即可),或是左链接(此时我们需要进行右旋转然后再转换),或是中链接(此时需要先左旋转然后再右旋转,最后转换颜色)。颜色转换会使中间结点的颜色变红,相当于将它送入了父结点。这意味着父结点中继续插入一个新键,我们只需要使用相同的方法解决即可,直到遇到一个2-结点或者根结点为止。
例如:
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第36张图片
插入H
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第37张图片
此时你会发现,S的左子结点和它左子结点的左子结点都为红链接,所以需要进行右旋操作
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第38张图片
右旋过后,R的左右子结点都是红链接,需要进行颜色反转操作
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第39张图片
虽然经过颜色反转,但此时会发现E结点和其右子结点间的连接为红链接,仍不满足红黑树定义,需要进行左旋操作
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第40张图片

上面就是红黑树的简单介绍,下面是红黑树的具体实现:

API设计

每个结点都只会有一条指向自己的链接(从父结点指向它),因为红黑树和二叉查找树很相似,所以需要左子结点,右子结点,键,值,四个变量,但红黑树区别在于多了一个boolean类型的变量color来表示链接的颜色,如果指向结点的链接为红色,则变量值为true,如果链接是黑色的,那么变量值为false。

public class RedBlackTree<Key extends Comparable<Key>,Value> {

    private Node root; //根结点
    private int N; //树中元素个数
    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 N;
    }

    //判断当前结点的父指向链接是否为红色
    private boolean isRed(Node x){
        if(x==null){
            return false;
        }
        return x.color == RED;
    }

    //左旋操作
    private Node rotateLeft(Node h){
        //获取h结点的右子结点,表示为x
        Node x = h.right;
        //让x结点的左子结点成为h结点的右子结点
        h.right = x.left;
        //让h成为x结点的左子结点
        x.left = h;
        //让x结点的color属性等于h结点的color属性
        x.color = h.color;
        //让h结点的color属性变为红色
        h.color = RED;

        return x;
    }

    //右旋操作
    private Node rotateRight(Node h){
        //获取h结点的左子结点,表示为x
        Node x = h.left;
        //让x结点的右子结点成为h结点的左子结点
        h.left = x.right;
        //让h结点成为x结点的右子结点
        x.right = h;
        //让x结点的color属性等于h结点的color属性
        x.color = h.color;
        //让h结点的color属性为红色
        h.color = RED;
        return x;
    }

    //颜色反转操作
    private void flipColor(Node h){
        //当前结点变为红色
        h.color = RED;
        //左子结点和右子结点变为黑色
        h.left.color = BLACK;
        h.right.color = BLACK;
    }

    //插入操作
    public void put(Key key, Value value){
       root = put(root,key,value);

        //让根结点的颜色保持黑色
        root.color = BLACK;
    }

    private Node put(Node h, Key key, Value value){
        //判断h是否为空,为空则返回一个红色链接的结点
        if(h == null){
            N++;
            return new Node(key,value,null,null, RED);
        }

        //不为空则比较h结点的键和key的大小
        int cmp = key.compareTo(h.key);
        if(cmp < 0){
            //继续往左
            h.left = put(h.left,key,value);
        }else if(cmp > 0){
            //继续往右
            h.right = put(h.right,key,value);
        }else{
            h.value = value;
        }

        //进行左旋:当当前结点h的左子结点为黑色,右子结点为红色,需要左旋
        if(isRed(h.right) && !isRed(h.left)){
            h = rotateLeft(h);
        }
        //进行右旋:当当前结点h的左子结点和左子结点的左子结点都为红色时需要右旋
        if(isRed(h.left) && isRed(h.left.left)){
            h = rotateRight(h);
        }
        //颜色反转:当前结点的左子结点和右子结点都为红色时需要颜色反转
        if(isRed(h.left) && isRed(h.right)){
            flipColor(h);
        }
        return h;
    }

    public Value get(Key key){
        return get(root,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;
        }
    }
}

B树

介绍

B-tree树即B树,B即Balanced,平衡的意思。有人把B-tree翻译成B-树,容易让人产生误解。会以为B-树是一种树,而B树又是另一种树。实际上,B-tree就是指的B树。是一种树状数据结构,它能够存储数据、对其进行排序并允许以o(logn)的时间复杂度进行查找、顺序读取、插入和删除等操作。
B树前面所介绍的2-3树、红黑树以及与2-3树相似的2-3-4树其实都是B树。此外在学习mysql时,可以经常听到某种类型的索引是基于B树或者B+树的。

B树的特点

(1)B树的阶:结点的最多子结点个数,如2-3树的阶是3
(2)B树的搜索:从根结点开始,对接点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进行查询关键字所属范围的儿子结点;重复操作,直到所对应的儿子指针为空,或已是叶子结点。
(3)关键字集合分布在整棵树中,即叶子结点和非叶子结点都存放数据
(4)搜索性能等价于在关键字全集内做一次二分查找

B树中允许一个结点中包含多个key,如果我们选择一个参数M,来构造一个B树,把它称作是M阶的B树,那么该树会具有如下特点:
(1)每个结点最多有M-1个key,并且以升序排序;
(2)每个结点最多能有M个子结点;
(3)根结点至少有两个子结点;

在实际应用中B树的阶数一般都比较大(通常大于100),即便存储大量数据,B树的高度仍然比较小,这样在某些应用下可以发挥它的优势。

B树的构造(插入)

假设参数M为5,name每个结点最多包含4个键值对,如下,
(1)在空树中插入39
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第41张图片

(2)继续插入22,97和41
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第42张图片

(3)继续插入53
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第43张图片
(4)继续插入13,21
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第44张图片
(5)继续插入40
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第45张图片
(6)继续插入30,27,33,36,35,34,24,29
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第46张图片
(7)插入26
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第47张图片

可以注意到当插入26时,第一层就变为为22,26,33,36,41,需要将处于中间的那个数往上提作为根节点,这里中间数为33,所以将其作为根结点

B+树

概述

B+树时对B树的一种变形树,与B树的差别在于
(1)非叶子节点经具有索引作用,不存放值value
(2)树的所有叶子结点构成一个有序链表,可以按照key排序的次序遍历全部数据

B+树存储数据

看具体的图示来理解B+树存储数据的过程,如下
(1)在空树中插入5
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第48张图片
(2)继续插入8,10,15
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第49张图片
(3)继续插入16
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第50张图片

可以看到当把10往上提的时候,只是把它的索引往上提了,但它的索引和值依旧存储在它的子结点中,子结点间是链表关系,由小到大排序形成了一个链表

(4)继续插入17
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第51张图片

(5)继续插入18
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第52张图片

(6)继续插入6,9,19,20,21,22
在这里插入图片描述

(7)继续插入7
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第53张图片

B+树和B树的对比

1、B+树的优点
(1)B+树在非叶子结点上不包含真正的数据,只当做索引使用,所以在内存相同的情况下,可以存放更多的key
(2)B+树的叶子结点间是相连的,对整棵树的遍历只需要一次线性遍历叶子结点即可,同时也便于查找搜索(而B树是需要每一层都进行递归遍历的)
2、B树的优点
B树的每一个结点都包含有key和value,当我们根据key找value时只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找都必须一次次找到树的最大深度处,也就是叶子结点的深度,才能找到value。

B+树在数据库中的应用

例如有如数据存储在数据库中,这些数据并没有建立主键索引
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第54张图片
当执行select * from user where id = 18,查找id为18的数据时,需要从第一条数据开始,一直往下查询知道找到id=18的数据,此时才能查找到目标结果

当建立了主键索引查询时(通过B+树实现索引查询)
数据结构与算法(java):树-平衡树(2-3树原理,红黑树,B树,B+树)_第55张图片

此时我们查询id=18的数据的步骤就变为:
拿18和根结点中的索引12进行比较,18大,找12的右子结点,从12的右子结点中从左往右继续找,哎,找到了18,通过索引18处的地址值,找到地址值中对应的值


基本记录的都是思路,具体实现看自己(懒)

你可能感兴趣的:(Java,数据结构与算法(java),b树,数据结构,算法)