数据结构与算法之树(2)

一. 平衡树

  • 前面我们学过二叉查找树,如果像之前那样依次按照9,8,7,6,5,4,3,2,1的顺序插入数据的话,可能会得到下图的树,这样我们如果要查找1这个元素效率非常低,因为需要查找所有元素最后才能找到1.效率低的原因在于这个是并不平衡,而是全部都是右分支,如果我们有一种方法,能够不受插入数据的影响,让生成的树都像完全二叉树那样,那么即使在糟糕的情况下效果依旧很好。
    在这里插入图片描述

1.1 2-3查找树

  • 为了解决上述问题,我允许树种的结点可以保存多个键,确切的说我们将一棵标准的二叉树中的结点称为2-结点(含有一个键和两条链),现在我们引入3-结点,它含有两个键和三条链。2-结点和3-结点中的每条链都对应这着其中分割所产生的区间。
1.1.1 2-3查找树的定义
  • 一棵2-3查找树要么为空,要么满足下面两个要求:
    • 2-结点:含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右连接指向的2-3树中的键都大于该结点。
    • 3-结点:含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
      在这里插入图片描述
1.1.2 2-3树的查找
  • 将二叉查找树的查找算法一般化我们就能够直接得到2-3树的查找算法。要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较结果找到指向的相应区间的链接,并在其指向的子树中递归地继续查找。如果这个是空链接,查找未命中。
    在这里插入图片描述
1.1.3 插入
  • 向2-结点中插入新键:如果查找后未找到的结点是个2-结点,那么很容易,我们只需要将这个2-结点变为3-结点就行。
    在这里插入图片描述
  • 向一棵只含有一个3-结点的树中插入新键:假如2-3树只包含一个3-结点,这个结点有两个键,没有空间来插入第三个键了,最自然的方式是我们假设这个结点能存放三个元素,暂时使其变成一个4-结点,同时它包含四条链接。然后,将这个4-结点的中间元素提升,左边的键作为其左子节点,右边的键作为其右子节点。插入完成,变为平衡2-3查找树,树的高度从0变为1;
    在这里插入图片描述
  • 向一个父结点未2-结点的3-结点中插入新的元素:将新的元素插入到3-结点中,使其成为临时的4-结点,然后,将该结点中的中间元素提升到父结点即2-结点中,使其父结点成为一个3-结点,然后将左右结点分别挂在这个3-结点的恰当位置。
    在这里插入图片描述
  • 向一个父结点为3-结点的3-结点中插入新键:当我们向一个3-结点插入的时候,我们将该结点拆分,中间元素提升至父结点,又因为父结点是一个3-结点,所以父结点被临时提升为一个4-结点,然后继续拆分,直到遇到父结点为2-结点的,将其变为3-结点不需要进行拆分。
    在这里插入图片描述
  • 分解根结点:当插入的结点到根节点的路径上全部是3-结点时,将根节点临时变为一个4-结点,然后将根节点拆分为两个2-结点,树的高度+1
    在这里插入图片描述
1.1.4 2-3树的性质
  • 通过对2-3树插入操作的分析,我们发现在插入的时候,2-3树需要做一些局部的变换来保持2-3树的平衡。一颗完全平衡的2-3树具有以下性质:
    • 任意空连接到根结点的路径长度都是相等的
    • 4-结点变为3-结点时,树的高度不会发生变化,只有当根节点是临时的4-结点,分解根结点时,树高+1
    • 2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长的,而2-3树是自底向上生长的。

1.2 红黑树

  • 红黑树对2-3进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:
    • 红链接:将两个2-结点连接起来构成一个3-结点。
    • 黑链接:是2-3树中的普通链接。
  • 在红黑树中我们将3-结点表示为由一条左斜的红色链接(两个2-结点其中之一是另一个左子结点)相连的两个2-结点。这种表示方法的一个优点是,我们无需修改就可以直接使用标准的二叉查找树的get方法。
    在这里插入图片描述
1.2.1 红黑树的定义
  • 红黑树是含有红黑链接并满足下列条件的二叉查找树:
    • 红链接均为左连接
    • 没有任何一个结点同时和两条红结点相连
    • 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。
      在这里插入图片描述
1.2.2 红黑树结点API
  • 因为每个结点都只会有一条指向自己的链接(从父结点指向它),我们可以在之前的Node结点中添加一个布尔类型的变量color来表示链接的颜色。如果指向它的链接是红色的,那么该变量的值为true,如果链接是黑色的,那么该变量的值为false;
    在这里插入图片描述
    在这里插入图片描述
1.2.3 平衡化
  • 在对红黑树进行一些增删改查的操作后,很有可能会出现红色和右连接或者两条连续的红色的链接,而这些都不满足红黑树的定义,所以我们需要对这些情况通过旋转进行修复,让红黑树保持平衡
1.2.3.1 左旋
  • 当某个结点的左子节点为黑色,右子节点为红色,此时需要左旋
  • 左旋过程:
    • 前提:当前结点为h,它的右子节点为x
    • 让x的左子结点变为h的右子节点:h.right=x.left
    • 让h成为x的左子节点:x.left=h
    • 让h的color属性变为x的color属性值:x.color=h.color
    • 让h的color属性变为RED: h.color=true
      在这里插入图片描述
1.2.3.1 右旋
  • 当某个结点的左子结点是红色,并且左子结点的左子结点也是红色,需要右旋
  • 右旋过程
    • 前提:当前结点为h,它的左子结点为x
    • 让x的右子节点成为h的左子结点:h.left=x.right
    • 让h成为x的右子结点: x.right=h
    • 让x的color变为h的color属性值:x.color=h.color
    • 让h的color为RED
      在这里插入图片描述
1.2.4 向单个2-结点中红插入新键
  • 一棵只含有一个键的红黑树只含有一个2-结点。插入另一个键后,我们马上就需要将他们旋转。
  • 如果新键小于当前结点的键,我们只需要增加一个红色结点即可,新的红黑树和单个3-结点完全等价。
    在这里插入图片描述
  • 如果新键大于当前节点的键,那么新增的红色结点将会产生一条红色的右链接,此时我们需要通过左旋,把红色右连接变为左连接,插入操作才算完成。形成的新的红黑树依然和3-结点等价,其中含有两个键,一个红色链接。
    在这里插入图片描述
1.2.5 向底部的2-结点插入新键
  • 用和二叉查找树相同的方式向一个棵红黑树中插入一个新键,会在树的底部新增一个结点(可以确保有序性),唯一区别的地方是我们会用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种方式仍然适用。
    在这里插入图片描述
1.2.6 颜色反转
  • 当一个结点的左子结点和右子结点的color都是RED时,也就是出现了临时的4-结点,此时只需要把左子结点和右子节点的颜色变为BLACK,同时让当前结点的颜色变为RED即可。
    在这里插入图片描述
1.2.7 向一棵双键树(即一个3-结点)中插入新键
  • 这种情况可以分为三种情况:
  • 新键大于原树中的两个键:
    在这里插入图片描述
  • 新键小于原树中的两个键
    在这里插入图片描述
  • 新键介于原数中的两个键之间
    在这里插入图片描述
1.2.8 根结点的颜色总是黑色
  • 因为颜色是表示父结点指向该结点的链接的颜色,根结点没有父结点,所以每次插入操作之后都将父结点的颜色置为黑色。
1.2.9 向树底部的3-结点插入新键
  • 假设在树的底部的一个3-结点下加入一个新的结点。前面的三种情况都会出现。指向新结点的链接可能是3-结点的右连接(此时只需要转换颜色即可),或是左链接(此时我们需要进行右旋转然后在转换),或是中结点(此时需要先左旋转然后再右旋转,最后转换颜色)。颜色转换会使中间结点的颜色变红,相当于将它送入了父结点。这意味着父结点中继续插入一个新键,我们只需要使用相同的方法解决即可,直到遇到一个2-结点或者根结点为止。
    在这里插入图片描述
1.2.10 红黑树的API设计

在这里插入图片描述

1.2.11 红黑树代码实现
package com.tiger.study.DataStructure.Tree;

import com.sun.org.apache.regexp.internal.RE;

import javax.xml.soap.Node;

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;

    // 获取树中元素个数
    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;
    }

    // 颜色反转,相当于完成拆分4-结点
    private void flipColors(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 val) {
        // 判断h是否为空,如果为空则直接返回一个红色的结点就可以了
        if (h == null) {
            N++;
            return new Node(key, val, null, null, RED);
        }

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

        // 进行左旋,当当前结点的左子结点为黑色,右子结点为红色,需要左旋
        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)) {
            flipColors(h);
        }

        return h;
    }

    // 根据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;
        }
    }


    // 结点类
    public 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;
        }

    }
}

二. B-树

  • B树是一种树状数据结构,它能够存储数据,对其进行排序并以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等。
2.1 B树的特性
  • B树允许一个结点中包含多个key,可以是3个、4个、5个甚至更多,并不确定,需要看具体的实现。现在我们选择要给参数M,来构造一个B树,我们可以把它称作是:M阶B树,那么该树有以下特点:
    • 每个结点最多有M-1个key,并且以升序排列
    • 每个结点最多能有M个子结点
    • B结点至少有两个子结点
      在这里插入图片描述
2.2 B树存储数据
  • 若参数M选择为5,那么每个结点最多包含4个键值对,我们以5阶B树为例:
    在这里插入图片描述

三. B+树

  • B+树是对B树的一种变形树,它与B树的差异在于:
    • 非叶子结点仅仅具有索引作用,也就是说非叶子结点只存储key,不存储value
    • 树的所有叶子节点构成一个有序链表,可以按照key排序的次序遍历全部数据
3.1 B+树存储数据
  • 选择参数M为5,每个结点最多包含4个键值对,我们以5阶B+树为例:
    在这里插入图片描述
3.2 B+树和B树对比
  • B+树优点:
    • 由于B+树在非叶子节点上不包含真正的数据,当作索引使用,因此在内存相同的情况下,能够存放更多的key。
    • B+树的叶子结点是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树需要进行每层遍历。
  • B树的优点
    • 由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值