JAVA之数据与结构详解

数据结构和算法

1.数据结构

  数据结构是计算机存储组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。接下来分别介绍下常见的数据结构类型。

1.1 线性结构

1.1.1 数组

  数组(Array)是一种线性表数据结构。它用于存储具有固定大小的相同类型的数据元素。在数组中,数据元素按照有序的方式进行排列,可以通过索引访问数组中的任意位置的元素。

// 动态初始化:初始化时由程序员只指定数组长度,由系统为数组元素分配初始值
char c1[] = new char[5];
// 静态初始化: 初始化时由程序员显示置顶每个数组的初始值,由系统决定数组长度
char c2[] = new char[]{'E','D','U','Y','U'};
char c3[] = {'E','D','U','Y','U'};

数组的特点如下:

  1. 顺序存储:数组中的元素按照顺序存储在连续的内存空间中,每个元素都有一个唯一的索引,可以通过索引快速访问。
  2. 大小固定:一旦定义了数组的大小,就不能改变。如果需要更大的存储空间,需要重新定义一个新的数组。
  3. 元素类型相同:数组中的所有元素必须是相同的数据类型。
  4. 无界数组:数组的长度可以是任意的整数,只要内存空间足够。

JAVA之数据与结构详解_第1张图片

数组的优点:

  1. 访问速度快:由于数组是顺序存储的,可以通过索引直接访问数组中的元素,时间复杂度为O(1)。
  2. 易于实现:数组是一种简单的数据结构,容易实现和操作。

数组的缺点:

  1. 大小固定:数组的大小是固定的,不能动态扩展。如果需要更多的存储空间,需要重新定义一个新的数组,这会增加额外的开销。
  2. 空间利用率低:由于数组是连续的内存空间,即使某些位置没有被使用,也不能被其他数据结构使用,导致空间利用率较低。
1.1.2 队列

  队列是一种特殊的数据结构,其特点是遵循先进先出(FIFO)的原则。队列中的元素只能从一端(称为队尾)添加,而从另一端(称为队头)删除。

队列的特点如下:

  1. 先进先出:队列中的元素遵循先进先出的原则,即最早进入队列的元素最先被删除。
  2. 插入和删除操作发生在同端:队列中的插入操作发生在队尾,删除操作发生在队头。
  3. 无界队列:队列的长度可以是任意的整数,只要内存空间足够。

JAVA之数据与结构详解_第2张图片

Java代码的具体实现

public static void main(String[] args) {
    Queue<Integer> queue = new LinkedList<>();
    queue.offer(3);//尾插
    queue.offer(6);
    queue.offer(9);
    queue.offer(12);
    System.out.println(queue);
    System.out.println(queue.peek());//访问队列头元素
    System.out.println(queue);
    System.out.println(queue.poll());//删除队列头元素
    System.out.println(queue);
}
1.1.3 链表

  链表是一种常见的数据结构,它通过指针将一组零散的内存块串联在一起。链表中的每个内存块被称为节点,每个节点除了存储数据之外,还需要记录链上的下一个节点的地址。

链表的特点是:

  1. 不需要连续的内存空间。
  2. 有指针引用。
  3. 插入、删除数据效率高,时间复杂度为O(1)级别(只需更改指针指向即可);但是,随机访问效率低,时间复杂度O(n)级别(需要从链头至链尾进行遍历)。
  4. 和数组相比,内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针。

链表包括单向链表双向链表循环链表等类型。其中,单向链表的节点只有一个后继指针next指向后面的节点;双向链表的节点除了有一个后继指针next指向后面的节点外,还有一个前驱指针prev指向前面的节点;循环链表与单向链表的唯一区别是尾节点的指针指向头节点,形成一个环。

在这里插入图片描述

在这里插入图片描述

1.1.4 栈

  栈(Stack)是一种后进先出(LIFO)的数据结构,它只能在一端进行插入和删除操作,这一端被称为栈顶,另一端被称为栈底。栈的元素之间存在一种顺序关系,这种顺序关系由元素的插入和删除操作决定。

栈的主要操作有:

  1. 入栈(push):在栈顶添加一个元素。
  2. 出栈(pop):删除栈顶的元素并返回其值。
  3. 判断栈空(is_empty):检查栈是否为空。
  4. 获取栈顶元素(top):返回栈顶的元素值,但不删除它。

JAVA之数据与结构详解_第3张图片

1.2 非线性结构

  非线性表:与线性表对立,在非线性表之中,数据之间并不是简单的前后关系。非线性结构是一种相对复杂的数据结构,它不满足线性结构的数据元素之间的一对一关系。非线性结构包括图结构、树结构、二维数组、广义表、多维数组等。

  在非线性结构中,数据元素之间存在多对多的关系,这种关系可以通过指针、引用等来实现。非线性结构可以用来表示复杂的数据关系,例如网络关系、图形关系等。

  本课程中我们介绍下和Java关联性比较强的非线性结构-树

1.2.1 树

[Tree]是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:

  1. 有且仅有一个特定的称为根[Root]的结点;
  2. 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…、Tn,其中每一个集合本身又是一棵树,并且称为根的子树。

  此外,树的定义还需要强调以下两点:

  1. 根结点是唯一的,不可能存在多个根结点,数据结构中的树只能有一个根结点。
  2. 子树的个数没有限制,但它们一定是互不相交的。

如图,是一棵普通树

JAVA之数据与结构详解_第4张图片

度数:结点拥有的子树数目称为结点的

JAVA之数据与结构详解_第5张图片

节点关系:

  • 孩子结点
  • 双亲结点
  • 兄弟结点

节点层次:

  从根开始定义起,根为第一层,根的孩子为第二层,以此类推。

JAVA之数据与结构详解_第6张图片

树的深度:树中结点的最大层次,如上图深度为4

根据不同的分类方式,树可以分为不同的类型:

  1. 根据树分支的数量限制:可以分为二叉树和多叉树。二叉树最多只有两个子节点,而多叉树一个节点可以有多于两个的子节点。
  2. 根据树节点的有序性:可以分为查找树和无序树。查找树的基本特征为任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。无序树则没有特定的键值大小关系。
  3. 根据具体用途和特征:例如红黑树AVL树平衡二叉树平衡二叉搜索树等。红黑树是一种自平衡二叉查找树,AVL树也是一种自平衡二叉查找树,它要求任何节点的两个子树的高度差最大为1。平衡二叉树和平衡二叉搜索树则是为了平衡树的左右子树的高度差。
  4. 根据树的完整性和是否包含空值:可以分为完全二叉树、满二叉树、完全二叉搜索树、满二叉搜索树等。完全二叉树和满二叉树是包含所有节点的二叉树,而完全二叉搜索树和满二叉搜索树则是所有节点都按照一定顺序排列的二叉搜索树。
1.2.2 二叉树

  每个子节点只有两个节点的树,每个结点至多拥有两棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。

JAVA之数据与结构详解_第7张图片

二叉查找树也称为有序二叉查找树,满足二叉查找树的一般性质,是指一棵树具有如下性质:

  1. 任意节点左子树不为空,则左子树的值均小于根节点的值
  2. 任意节点右子树不为空,则右子树的值均大于于根节点的值
  3. 任意节点的左右子树也分别是二叉查找树
  4. 没有键值相等的节点

二叉树又分为:

  • 完美二叉树
  • 完全二叉树
  • 完满二叉树

完美二叉树:又称为 满二叉树 ,除了叶子节点之外的每一个节点都有两个孩子节点,每层都被完全填充

JAVA之数据与结构详解_第8张图片

完全二叉树:除了最后一层之外的其他每一层都被完全填充,并且所有的节点都保持向左对齐

JAVA之数据与结构详解_第9张图片

完满二叉树:除了叶子节点之外的每一个节点都有两个孩子节点。

JAVA之数据与结构详解_第10张图片

二叉树的遍历操作

二叉树中的遍历规则有如下三种:

中序遍历:所谓的中序遍历就是先访问左节点,再访问根节点,最后访问右节点,即左-根-右遍历

先序遍历:所谓的前序遍历就是先访问根节点,再访问左节点,最后访问右节点,即根-左-右遍历(前序)

后序遍历:所谓的后序遍历就是先访问左节点,再访问右节点,最后访问根节点。即左-右-根遍历

JAVA之数据与结构详解_第11张图片

查找最小值:沿着根节点的左子树一路查找,直到最后一个不为空的节点,该节点就是当前这个树的最小节点

查找最大值:沿着根节点的右子树一路查找,直到最后一个不为空的节点,该节点就是当前这个树的最大节点

查找前驱节点 :小于当前节点的最大值

查找后继节点 :大于当前节点的最小值

JAVA之数据与结构详解_第12张图片

二叉树的删除操作:

二叉树中的删除节点:本质上是找前驱节点或者后继节点来替代

  • 叶子节点直接删除
  • 只有一个子节点的用子节点替代(本质上就是找的前驱节点或者后继节点,左节点就是前驱节点,右节点就是后继节点)
  • 有两个子节点的,需要找到替代节点(替代节点就是前驱节点或者后继节点)

二叉树的查找的局限性:

一个二叉查找树是由n个节点随机构成,所以,对于某些情况,二叉查找树会退化成一个有n个节点的线性链.如下图:

JAVA之数据与结构详解_第13张图片

1.2.3 AVL树

  BST存在的问题是,树在插入的时候会导致倾斜,不同的插入顺序会导致数的高度不一样,而树的高度直接影响了树的查找效率。最坏的情况所有的节点都在一条斜线上,这样树的高度为N。基于BST存在的问题,平衡查找二叉树(Balanced BST)产生了。平衡树的插入和删除的时候,会通过旋转操作将高度保持在LogN。其中两款具有代表性的平衡术分别为AVL树(高度平衡树,具备二叉搜索树的全部特性,而且左右子树高度差不超过1)和红黑树。

AVL树是如何实现平衡的呢?,具体是通过左旋或者右旋来实现的。具体如下图:

JAVA之数据与结构详解_第14张图片

  虽然AVL可以解决二叉树所存在的问题,但是AVL树要求太过严格,左旋和右旋的开销会比较大,这时出现了红黑树,只要求黑色节点平衡即可.

1.2.4 2-3-4树

  2-3-4树是四阶的 B树(Balance Tree),他属于一种多路查找树,它的结构有以下限制:所有叶子节点都拥有相同的深度。节点只能是 2-节点、3-节点、4-节点之一。

  • 2-节点:包含 1 个元素的节点,有 2 个子节点;
  • 3-节点:包含 2 个元素的节点,有 3 个子节点;
  • 4-节点:包含 3 个元素的节点,有 4 个子节点;

  所有节点必须至少包含1个元素,元素始终保持排序顺序,整体上保持二叉查找树的性质,即父结点大于左子结点,小于右子结点;而且结点有多个元素时,每个元素必须大于它左边的和它的左子树中元素。

下图是一个典型的 2-3-4树:

JAVA之数据与结构详解_第15张图片

生成的过程
  接下来我们通过演示来看看2-3-4树生成的过程
第一次插入—2节点

在这里插入图片描述

插入第二个节点–3节点 合并

在这里插入图片描述

插入第三个节点—4节点 合并

在这里插入图片描述

插入第4个节点—需要分裂

JAVA之数据与结构详解_第16张图片

插入6

JAVA之数据与结构详解_第17张图片

插入7

JAVA之数据与结构详解_第18张图片

插入8

JAVA之数据与结构详解_第19张图片

插入9

JAVA之数据与结构详解_第20张图片

插入10

JAVA之数据与结构详解_第21张图片

插入11

JAVA之数据与结构详解_第22张图片

插入12

JAVA之数据与结构详解_第23张图片

最后我们插入1来看看效果

JAVA之数据与结构详解_第24张图片

  到这儿相信大家对于2-3-4树应该有了个直观的认知了。然后来看看2-3-4树红黑树的对应关系。这个能帮助我们更好的理解红黑树.

  红黑树起源于2-3-4树,它的本质就是2-3-4树。

2节点:一个2节点对应的红黑树节点就是一个黑色节点

JAVA之数据与结构详解_第25张图片

3节点:一个三节点可以有两种情况的红黑树节点,一种是右倾,一种是左倾,所以一个2-3-4树可以有多个红黑树

JAVA之数据与结构详解_第26张图片

原则:上黑下红

4节点:一个四节点转换的情况只有一种,中间节点黑色,左右节点红色

JAVA之数据与结构详解_第27张图片

裂变状态:还有就是在2-3-4树中存在的裂变状态。转换为红黑树后会先变色(不能有两个相邻的红色节点)。

JAVA之数据与结构详解_第28张图片

转换为红黑树:接下来具体看看一个2-3-4树是如何转换为对应的红黑树的,

原始的2-3-4树:

JAVA之数据与结构详解_第29张图片

  按照右倾规则来转换为:

JAVA之数据与结构详解_第30张图片

  转换后满足黑色节点平衡的要求
  按照左倾规则来转换为:

JAVA之数据与结构详解_第31张图片

  通过对2-3-4树和红黑树的等价关系,对于我们后面分析红黑树的内容会非常有帮助!!!

1.2.5 红黑树

  红黑树,Red-Black Tree 「RBT」是一个自平衡(不是绝对的平衡)的二叉查找树(BST),树上的每个节点都遵循下面的规则:

  1. 每个节点要么是黑色,要么是红色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL)是黑色。
  4. 每个红色结点的两个子结点一定都是黑色。
  5. 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

  红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色

操作 描述
左旋 以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,
右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
右旋 以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,
左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
变色 结点的颜色由红变黑或由黑变红。

旋转操作

  左旋:以某个节点作为旋转点,其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,左子节点保持不变。

JAVA之数据与结构详解_第32张图片

  右旋:以某!个节点作为旋转点,其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,右子节点保持不变。

JAVA之数据与结构详解_第33张图片

Java代码实现旋转

  先进行类结构定义

package com.bobo.util.treemap;

public class BRTree {

    private static final boolean RED = false;
    private static final boolean BLACK = true;

    private RBNode root;

    public RBNode getRoot() {
        return root;
    }

    public void setRoot(RBNode root) {
        this.root = root;
    }


    /**
     * 表示 节点
     * @param 
     * @param 
     */
    static class RBNode<K extends Comparable<K>,V>{
        // 节点是双向的
        private RBNode parent;
        private RBNode left;
        private RBNode right;
        private boolean color;
        private K key;
        private V value;

        public RBNode() {
        }

        public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
            this.parent = parent;
            this.left = left;
            this.right = right;
            this.color = color;
            this.key = key;
            this.value = value;
        }

        public RBNode getParent() {
            return parent;
        }

        public void setParent(RBNode parent) {
            this.parent = parent;
        }

        public RBNode getLeft() {
            return left;
        }

        public void setLeft(RBNode left) {
            this.left = left;
        }

        public RBNode getRight() {
            return right;
        }

        public void setRight(RBNode right) {
            this.right = right;
        }

        public boolean isColor() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
}

左旋代码实现

    /**
     * 围绕p左旋
     *     p               pr(r)
     *    / |             / \
     *   pl  pr(r) =>    p   rr
     *      / \         / \
     *     rl  rr     pl  rl
     *
     *  左旋的时候
     *  p-pl 和 pr-rr的关系不变
     *  pr-rl 要变为 p-rl
     *      也就是 rl要变为 p的右子节点
     *      同时 p要成为 rl 的父节点
     *  还有就是要判断  p 是否有父节点
     *  如果没有
     *     r 变为 root 节点
     *  如果有
     *     r.parent = p.parent
     *     还要设置 r为 p.parent 的子节点(可能左也可能右)
     *     如果 p.parent.left == p
     *        p.parent.left = r;
     *     否则
     *        p.parent.right = r;
     *    最后
     *       p.parent = r;
     *       r.left = p;
     * @param p
     */
    private void leftRotate(RBNode p){
        if(p != null){
            RBNode r = p.right;
            // 1.设置 pr-rl 要变为 p-rl
            // 把rl设置到p的右子节点
            p.right = r.left;
            if(r.left != null){
                // 设置rl的父节点为p
                r.left.parent = p;
            }
            // 2.判断p的父节点情况
            r.parent = p.parent; // 不管 p是否有父节点,都把这个父节点设置为 r的父节点
            if(p.parent == null){
                root = r; // p没有父节点 则r为root节点
            }else if(p.parent.left == p){
                p.parent.left = r; // 如果p为 p.parent的左子节点 则 r 也为 p.parent的左子节点
            }else{
                p.parent.right = r; // 反之设置 r 为 p.parent的右子节点
            }
            // 最后 设置 p 为 r 的左子节点
            r.left = p;
            p.parent = r;
        }
    }

右旋实现:

    /**
     * 围绕p右旋
     * @param p
     */
    public void rightRotate(RBNode p){
        if(p != null){
            RBNode r = p.left;
            p.left = r.right;
            if(r.right != null){
                r.right.parent = p;
            }
            r.parent = p.parent;
            if(p.parent == null){
                root = r;
            }else if(p.parent.left == p){
                p.parent.left = r;
            }else{
                p.parent.right = r;
            }
            r.right = p;
            p.parent = r;
        }
    }

  2-3-4树中结点添加需要遵守以下规则:

  • 插入都是向最下面一层插入
  • 升元:将插入结点由 2-结点升级成 3-结点,或由 3-结点升级成 4-结点;
  • 向 4-结点插入元素后,需要将中间元素提到父结点升元,原结点变成两个 2-结点,再把元素插入2-结点中,如果父结点也是 4-结点,则递归向上层升元,至到根结点后将树高加1;

  而将这些规则对应到红黑树里,就是:

  • 新插入的结点颜色为 红色 ,这样才可能不会对红黑树的高度产生影响。
  • 2-结点对应红黑树中的单个黑色结点,插入时直接成功(对应 2-结点升元)。
  • 3-结点对应红黑树中的 黑+红 子树,插入后将其修复成 红+黑+红 子树(对应 3-结点升元);
  • 4-结点对应红黑树中的 红+黑+红 子树,插入后将其修复成 红色祖父+黑色父叔+红色孩子 子树,然后再把祖父结点当成新插入的红色结点递归向上层修复,直至修复成功或遇到 root 结点;

  公式:红黑树+新增一个节点(红色)**=**对等的2-3-4树+新增一个节点

新增节点案例

​   我们通过新增2-3-4树的过程来映射对应的红黑树的节点新增

JAVA之数据与结构详解_第34张图片

1.新增一个节点,2 节点

JAVA之数据与结构详解_第35张图片

2.新增一个节点,与2节点合并,直接合并

JAVA之数据与结构详解_第36张图片

3.新增一个节点,与3节点合并,直接合并

插入的值的位置会有3种情况

JAVA之数据与结构详解_第37张图片

对应的红黑树为:

JAVA之数据与结构详解_第38张图片

4.新增一个节点,与4节点合并,此时需要分裂

JAVA之数据与结构详解_第39张图片

插入值的位置可能是

JAVA之数据与结构详解_第40张图片

对应的红黑树的结构为:

JAVA之数据与结构详解_第41张图片

新增代码实现

  红黑树的新增规则我们理清楚了,接下来就可以通过Java代码来具体的实现了。

先实现插入节点,这就是一个普通的二叉树的插入

 /**
     * 新增节点
     * @param key
     * @param value
     */
    public void put(K key , V value){
        RBNode t = this.root;
        if(t == null){
            // 说明之前没有元素,现在插入的元素是第一个
            root = new RBNode<>(key , value == null ? key : value,null);
            return ;
        }
        int cmp ;
        // 寻找插入位置
        // 定义一个双亲指针
        RBNode parent;
        if(key == null){
            throw new NullPointerException();
        }
        // 沿着跟节点找插入位置
        do{
            parent = t;
            cmp = key.compareTo((K)t.key);
            if(cmp < 0){
                // 左侧找
                t = t.left;
            }else if(cmp > 0){
                // 右侧找
                t = t.right;
            }else{
                // 插入节点的值==比较的节点。值替换
                t.setValue(value==null?key:value);
                return;
            }
        }while (t != null);
        // 找到了插入的位置  parent指向 t 的父节点  t为null
        // 创建要插入的节点
        RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
        // 然后判断要插入的位置 是 parent的 左侧还是右侧
        if(cmp < 0){
            parent.left = e;
        }else{
            parent.right = e;
        }
        // 调整  变色 旋转
        fixAfterPut(e);
    }

然后再根据红黑树的特点来实现调整(旋转,变色)

private boolean colorOf(RBNode node){
        return node == null ? BLACK:node.color;
    }

    private RBNode parentOf(RBNode node){
        return node != null ? node.parent:null;
    }

    private RBNode leftOf(RBNode node){
        return node != null ? node.left:null;
    }

    private RBNode rightOf(RBNode node){
        return node != null ? node.right:null;
    }

    private void setColor(RBNode node ,boolean color){
        if(node != null){
            node.setColor(color);
        }
    }

    /**
     * 插入节点后的调整处理
     * 1. 2-3-4树 新增元素 2节点添加一个元素将变为3节点 直接合并,节点中有两个元素
     *      红黑树:新增一个红色节点,这个红色节点会添加在黑色节点下(2节点) --- 这种情况不需要调整
     * 2. 2-3-4树 新增元素 3节点添加一个元素变为4节点合并 节点中有3个元素
     *      这里有6中情况,( 根左左 根左右  根右右 根右左)这四种要调整  (左中右的两种)不需要调整
     *      红黑树:新增红色节点 会添加到 上黑下红的节点中 = 排序后中间节点是黑色,两边节点是红色
     *
     *  3. 2-3-4树:新增一个元素 4节点添加一个元素需要裂变:中间元素升级为父节点,新增元素与剩下的其中一个合并
     *      红黑树:新增节点是红色+爷爷节点是黑色,父亲节点和叔叔节点为红色 调整为
     *              爷爷节点变红色,父亲和叔叔节点变为黑色,如果爷爷节点为root节点则调整为黑色
     * @param x
     */
    private void fixAfterPut(RBNode<K, Object> x) {
        x.color = RED;
        // 本质上就是父节点是黑色的就不需要调整,对应的 2 3的情况
        while(x != null && x != root && x.parent.color == RED){
            // 1. x 的父节点是爷爷的 左孩子
            if(parentOf(x) == parentOf(parentOf(x)).left){
                // 获取当前节点的叔叔节点
                RBNode y = rightOf(parentOf(parentOf(x)));
                // 情况3
                if(colorOf(y) == RED){
                    // 说明是 上3的情况  变色处理
                    // 父亲节点和叔叔节点设置为黑色
                    setColor(parentOf(x),BLACK);
                    setColor(y,BLACK);
                    // 爷爷节点设置为 红色
                    setColor(parentOf(parentOf(x)),RED);
                    // 递归处理
                    x = parentOf(parentOf(x));
                }else{
                    // 情况 2
                    if(x == parentOf(x).right){
                        // 如果x是父节点的右节点那么我们需要先根据 父节点 左旋
                        x = parentOf(x);
                        leftRotate(x);
                    }
                    // 叔叔节点为空  对应于 上面的情况2
                    // 将父节点变为黑色
                    setColor(parentOf(x),BLACK);
                    // 将爷爷节点变为红色
                    setColor(parentOf(parentOf(x)),RED);
                    // 右旋转  根据爷爷节点右旋转
                    rightRotate(parentOf(parentOf(x)));

                }
            }else{
                // x 的父节点是爷爷是右孩子
                // 获取父亲的叔叔节点
                RBNode y = leftOf(parentOf(parentOf(x)));
                if(colorOf(y) == RED){
                    // 情况3
                    setColor(parentOf(x),BLACK);
                    setColor(y,BLACK);
                    setColor(parentOf(parentOf(x)),RED);
                    x = parentOf(parentOf(x));
                }else{
                    // 情况2
                    if( x == parentOf(x).left){
                        x = parentOf(x);
                        rightRotate(x);
                    }
                    setColor(parentOf(x),BLACK);
                    setColor(parentOf(parentOf(x)),RED);
                    leftRotate(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }

红黑树的删除操作:

  红黑树的节点的删除其实也分为两步:

  1. 先删除节点(这步和普通的二叉树删除是一样的)
  2. 然后再调整

  要删除这个节点先需要找到这个节点,找到节点就是普通的二分查找,具体代码如下

    private RBNode getNode(K key){
        RBNode node = this.root;
        while (node != null ){
            int cmp = key.compareTo((K) node.key);
            if(cmp < 0){
                // 在左子树
                node = node.left;
            }else if(cmp >0){
                // 右子树
                node = node.right;
            }else{
                return node;
            }
        }
        return null;
    }

  在整理红黑树节点的删除操作时我们需要先理解清楚红黑树删除和2-3-4树删除的等价关系,这样理解起来才会比较容易
  核心理论:红黑树删除操作的本质其实就是删除2-3-4树的叶子节点

JAVA之数据与结构详解_第42张图片

情况一

JAVA之数据与结构详解_第43张图片

情况2:删除的是非情况1的节点,根据我们前面介绍的删除的规则,会找到对应的前驱和后继节点,那么最终删除的还是叶子节点

JAVA之数据与结构详解_第44张图片

首先删除节点的代码为:

/**
     * 删除节点
     * @param key
     * @return
     */
    public V remove(K key){
        // 先找到这个节点
        RBNode node = getNode(key);
        if(node == null){
            return null;
        }
        // 把值存起来 删除后 返回
        V oldValue = (V) node.value;
        deleteNode(node);
        return oldValue;
    }

    /**
     * 删除节点
     *   3种情况
     * 1.删除叶子节点,直接删除
     * 2.删除的节点有一个子节点,那么用子节点来替代
     * 3.如果删除的节点右两个子节点,此时需要找到前驱节点或者后继节点来替代
     *    可以转换为 1、2的情况
     * @param node
     */
    private void deleteNode(RBNode node){
        // 3.node节点有两个子节点
        if(node.left !=null && node.right != null){
            // 找到要删除节点的后继节点
            RBNode successor = successor(node);
            // 然后用后继节点的信息覆盖掉 要删除节点的信息
            node.key = successor.key;
            node.value = successor.value;
            // 然后我们要删除的节点就变为了 后继节点
            node = successor;
        }
        // 2.删除有一个子节点的情况
        RBNode replacement = node.left != null ? node.left : node.right;
        if(replacement != null){
            // 替代者的父指针指向原来 node 的父节点
            replacement.parent = node.parent;
            if(node.parent == null){
                // 说明 node 是root节点
                root = replacement;
            }else if(node == node.parent.left){
                // 双向绑定
                node.parent.left = replacement;
            }else{
                node.parent.right = replacement;
            }
            // 将node的左右孩子指针和父指针都指向null node等待GC
            node.left = node.right = node.parent = null;
            // 替换完成后需要调整平衡
            if(node.color == BLACK){
                // fixAfterRemove(replacement)
            }
        }else if(node.parent == null){
            // 说明要删除的是root节点
            root = null;
        }else{
            // 1. node节点是叶子节点 replacement为null
            // 先调整
            if(node.color == BLACK){
                // fixAfterRemove(node)
            }
            // 再删除
            if(node.parent != null){
                if(node == node.parent.left){
                    node.parent.left = null;
                }else{
                    node.parent.right = null;
                }
                node = null;
            }
        }
    }

然后就是需要调整红黑树的平衡了。

删除后的平衡调整

1.情况一:自己能搞定的,对应叶子节点是3节点和4节点

JAVA之数据与结构详解_第45张图片

2.情况二:自己搞不定,需要兄弟借,但是兄弟不借,找父亲借,父亲下来,然后兄弟找一个人去代替父亲当家

这种情况就是兄弟节点是3节点或者4节点

找兄弟节点

JAVA之数据与结构详解_第46张图片

如果找到的兄弟节点是红色其实还要调整

JAVA之数据与结构详解_第47张图片

执行如下调整先,先变色,然后左旋

JAVA之数据与结构详解_第48张图片

JAVA之数据与结构详解_第49张图片

找兄弟节点借

JAVA之数据与结构详解_第50张图片

然后沿着7节点左旋

JAVA之数据与结构详解_第51张图片

3.情况三:跟兄弟借,兄弟也没有(情同手足,同时自损)

兄弟节点是2节点,同时当前节点的父节点是红色节点的情况

JAVA之数据与结构详解_第52张图片

删除后直接变色就可以了

兄弟节点是2节点,同时当前节点的父节点是黑色节点

JAVA之数据与结构详解_第53张图片

变更操作为如下,如果继续有父节点那么还要递归处理

JAVA之数据与结构详解_第54张图片

分析清楚了删除的3中情况,我们就可以撸处删除的调整的代码了

    /**
     * 2-3-4树删除操作:
     * 1.情况一:自己能搞定的,对应叶子节点是3节点和4节点
     * 2.情况二:自己搞不定,需要兄弟借,但是兄弟不借,找父亲借,父亲下来,然后兄弟找一个人去代替父亲当家
     * 3.情况三:跟兄弟借,兄弟也没有
     * @param x
     */
    private void fixAfterRemove(RBNode x){
        // 情况2、3
        while(x != root && colorOf(x) == BLACK){
            // 这种情况才需要调整
            // x 是左孩子的情况
            if(x == leftOf(parentOf(x))){
                // 找兄弟节点
                RBNode rNode = rightOf(parentOf(x));
                // 判断此时的兄弟节点是否是真正的兄弟节点 兄弟是红色的情况要调整
                if(colorOf(rNode) == RED){ // 2-3-4树的 3节点 交换颜色,然后左旋一次就可以了
                    setColor(rNode,BLACK);
                    setColor(parentOf(x),RED);
                    leftRotate(parentOf(x)); // 左旋一次
                    rNode = rightOf(parentOf(x)); // 找到真正的兄弟节点
                }
                // 情况3 找兄弟借 没得借
                if(colorOf(leftOf(rNode)) == BLACK && colorOf(rightOf(rNode)) == BLACK){
                    // 情况复杂
                    setColor(rNode,RED);
                    x=parentOf(x); // 向上递归
                }else{
                    // 情况2 找兄弟借 有借
                    // 兄弟节点是 3节点或者4节点
                    if(colorOf(rightOf(rNode)) == BLACK){
                        // 右孩子为空,则左孩子肯定不为空
                        // 兄弟节点 先要左一次右旋
                        setColor(rNode,RED);
                        setColor(leftOf(rNode),BLACK);
                        rightRotate(rNode);
                        // 重新调整叔叔节点的位置
                        rNode = rightOf(parentOf(x));
                    }
                    // 变色  兄弟节点是 3节点还是4节点 都旋转一次
                    setColor(rNode, colorOf(parentOf(x)));
                    setColor(parentOf(x),BLACK);
                    setColor(rightOf(rNode),BLACK);
                    // 左旋
                    leftRotate(parentOf(x));
                    x = root; // 结束循环  递归 针对的是 情况3
                }
            }else{
                // 找兄弟节点
                RBNode rNode = leftOf(parentOf(x));
                // 判断此时的兄弟节点是否是真正的兄弟节点 兄弟是红色的情况要调整
                if(colorOf(rNode) == RED){ // 2-3-4树的 3节点 交换颜色,然后左旋一次就可以了
                    setColor(rNode,BLACK);
                    setColor(parentOf(x),RED);
                    rightRotate(parentOf(x)); // 左旋一次
                    rNode = leftOf(parentOf(x)); // 找到真正的兄弟节点
                }
                // 情况3 找兄弟借 没得借
                if(colorOf(rightOf(rNode)) == BLACK && colorOf(leftOf(rNode)) == BLACK){
                    // 情况复杂
                    setColor(rNode,RED);
                    x=parentOf(x); // 向上递归
                }else{
                    // 情况2 找兄弟借 有借
                    // 兄弟节点是 3节点或者4节点
                    if(colorOf(leftOf(rNode)) == BLACK){
                        // 右孩子为空,则左孩子肯定不为空
                        // 兄弟节点 先要左一次右旋
                        setColor(rNode,RED);
                        setColor(leftOf(rNode),BLACK);
                        leftRotate(rNode);
                        // 重新调整叔叔节点的位置
                        rNode = leftOf(parentOf(x));
                    }
                    // 变色  兄弟节点是 3节点还是4节点 都旋转一次
                    setColor(rNode, colorOf(parentOf(x)));
                    setColor(parentOf(x),BLACK);
                    setColor(leftOf(rNode),BLACK);
                    // 左旋
                    rightRotate(parentOf(x));
                    x = root; // 结束循环  递归 针对的是 情况3
                }
            }
        }
        // 情况1:替代节点是红色,直接染黑  在情况3的情况下  补偿删除的黑色节点,这样红黑树依然保存平衡
        setColor(x,BLACK);
    }

好了。红黑树的内容大家应该搞清楚了。同时TreeMap的代码大家也搞清楚了。你可以看看TreeMap的代码其实和我们上面写的代码是一样的

1.2.6 B树

  B 树(Balanced Tree)这个就是我们的多路平衡查找树,叫做B-Tree(B代表平衡)。跟AVL树一样,B树在枝节点和叶子节点存储键值、数据地址、节点引用。
  它有一个特点:分叉数(路数)永远比关键字数多1。比如我们画的这棵树,每个节点存储两个关键字,那么就会有三个指针指向三个子节点。

JAVA之数据与结构详解_第55张图片

B Tree的查找规则是什么样的呢?
  比如我们要在这张表里面查找30。
  因为30小于36,走左边。
  因为30大于23,走右边。
  在磁盘块7里面就找到了30,只用了3次IO。

这个效率会比AVL树的效率更高

1.2.7 B+树

  加强版多路平衡查找树
  因为B Tree的这种特性非常适合用于做索引的数据结构,所以很多文件系统和数据库的索引都是基于B Tree的。
  但是实际上,MySQL里面使用的是B Tree的改良版本,叫做B+Tree(加强版多路平衡查找树)。

B+树的存储结构:

JAVA之数据与结构详解_第56张图片

MySQL中的B+Tree有几个特点:

  1. 它的关键字的数量是跟路数相等的;
  2. B+Tree的根节点和枝节点中都不会存储数据,只有叶子节点才存储数据。InnoDB 中 B+ 树深度一般为 1-3 层,它就能满足千万级的数据存储。搜索到关键字不会直接返回,会到最后一层的叶子节点。比如我们搜索id=28,虽然在第一层直接命中了,但是全部的数据在叶子节点上面,所以我还要继续往下搜索,一直到叶子节点。
  3. B+Tree的每个叶子节点增加了一个指向相邻叶子节点的指针,它的最后一个数据会指向下一个叶子节点的第一个数据,形成了一个有序链表的结构。

JAVA之数据与结构详解_第57张图片

总结一下, B+Tree的特点带来的优势:

  1. 它是B Tree的变种,B Tree能解决的问题,它都能解决。B Tree解决的两大问题是什么?(每个节点存储更多关键字;路数更多)
  2. 扫库、扫表能力更强(如果我们要对表进行全表扫描,只需要遍历叶子节点就可以了,不需要遍历整棵B+Tree拿到所有的数据)
  3. B+Tree的磁盘读写能力相对于B Tree来说更强(根节点和枝节点不保存数据区,所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多)
  4. 排序能力更强(因为叶子节点上有下一个数据区的指针,数据形成了链表)
  5. 效率更加稳定(B+Tree永远是在叶子节点拿到数据,所以IO次数是稳定的)

2.算法

  看完了基本的数据结构后我们还需要看看常用的一些算法。数据结构加算法就等于程序。

2.1 排序

  用于将一组数据按照特定的规则进行排序。排序算法可以分为内部排序和外部排序两种。

  1. 内部排序算法:

    • 冒泡排序(Bubble Sort):重复比较相邻的两个元素,如果顺序错误就交换位置,直到整个序列有序。
    • 插入排序(Insertion Sort):将待排序的元素插入已经排好序的序列中的正确位置,直到整个序列有序。
    • 选择排序(Selection Sort):每次从待排序序列中选择最小(或最大)的元素放到已排序序列的末尾,直到整个序列有序。
    • 快速排序(Quick Sort):选择一个基准元素,将比基准元素小的元素放在基准元素的左边,比基准元素大的元素放在基准元素的右边,然后递归地对左右两个子序列进行快速排序。
    • 归并排序(Merge Sort):将待排序序列划分为两个子序列,分别对两个子序列进行归并排序,然后将排序好的两个子序列合并成一个有序序列。
    • 堆排序(Heap Sort):将待排序序列构建成一个最大堆(或最小堆),然后依次取出堆顶元素,再对剩余元素进行堆调整,直到整个序列有序。
  2. 外部排序算法:

    • 多路归并排序:将待排序的数据分为多个有序的子序列,然后通过多次归并操作将这些子序列合并为一个有序序列。
    • 基于置换的排序:通过多次置换操作将待排序的数据重新排列成有序的序列。
    • 多层归并排序:将待排序的数据分成多个层次,每个层次都进行归并排序操作,最终得到一个有序序列。

不同的排序算法在时间复杂度、空间复杂度和稳定性等方面有所差异,选择合适的排序算法取决于具体的应用场景和数据规模。

2.2 查找

查找算法,也称为搜索算法,是一种用于在数据集中查找特定元素的算法。查找算法可以应用于各种数据结构,如数组、链表、树等。

常用的查找算法包括:

  1. 线性查找:顺序遍历数据集,逐个比较元素,直到找到目标元素或遍历完整个数据集。时间复杂度为O(n),其中n为数据集的大小。

  2. 二分查找:仅适用于已经排序的数据集。从数据集的中间元素开始比较,如果目标元素小于中间元素,则在左半部分继续查找;如果目标元素大于中间元素,则在右半部分继续查找;如果目标元素等于中间元素,则找到目标元素。时间复杂度为O(log n)。

  3. 哈希查找:通过哈希函数将目标元素映射到一个位置,然后在该位置进行查找。哈希查找的平均时间复杂度为O(1),但是在处理哈希冲突时可能需要线性查找。

  4. 二叉查找树:将数据集构建成二叉查找树,其中每个节点的左子树的值小于节点的值,右子树的值大于节点的值。通过比较目标元素和节点的值,可以在二叉查找树中进行快速查找。

  5. 平衡二叉查找树:在二叉查找树的基础上,通过旋转操作保持树的平衡,以提高查找效率。常用的平衡二叉查找树有红黑树、AVL树等。

  6. B树和B+树:适用于大规模数据的查找,将数据集分散存储在多个节点中,通过多级索引进行查找。B树和B+树的高度较小,能够减少磁盘I/O操作,提高查找效率。

  7. 字符串匹配算法:用于在文本中查找特定的字符串。常用的字符串匹配算法有暴力匹配算法、KMP算法、Boyer-Moore算法等。

你可能感兴趣的:(java,java,开发语言)