hashmap链表转化成红黑树的过程以及红黑树转化成链表的过程

2023-11-12

1.链表转红黑树的实现代码

// 该方法主要是将单向链表转化成双向链表(为了后面操作,比如在后面将红黑树放到数组上时,以及红黑树转成链表时简化操作)
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果数组为空或者数组大小小于最小数化的大小(64),则直接进行数据扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    	// 数组扩容的方法
        resize();
     // 获取要树化的链表的头结点,并判断是否为空
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        // 遍历要树化的整个链表,转化单向链表为双向链表
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 将数组中该位置的头指向转化成双向链表的头并判断是否非空
        if ((tab[index] = hd) != null)
        	// 树化该链表
            hd.treeify(tab);
    }
}
// 该方法是将链表转化成树,插入左右节点的依据是key的hash值去进行判断,如果插入元素的hash值大于节点的hash值,则插入到叶子节点的左边,如果小于则插入到右边,如果相等,则进行特殊处理(详情看后面)再去比较大小
final void treeify(Node<K,V>[] tab) {
   TreeNode<K,V> root = null;
   // 遍历转化的双向链表,此处this即为上面方法的hd(表头)
   for (TreeNode<K,V> x = this, next; x != null; x = next) {
       next = (TreeNode<K,V>)x.next;
       x.left = x.right = null;
       // 如果treeNode是空的(即第一个元素),则将元素置为根元素,且设置为黑节点
       if (root == null) {
           x.parent = null;
           x.red = false;
           root = x;
       }
       else {
       	   // 获取节点的key以及key的hash值
           K k = x.key;
           int h = x.hash;
           Class<?> kc = null;
           // 从红黑树根节点去从头遍历
           for (TreeNode<K,V> p = root;;) {
           		// 获取红黑树p节点的key以及key的hash值
               int dir, ph;
               K pk = p.key;
               // 比较要插入元素的hash值与红黑树节点p的大小,用dir去记录值
               if ((ph = p.hash) > h)
                   dir = -1;
               else if (ph < h)
                   dir = 1;
               // 当插入元素的hash值与红黑树节点p的hash值相等时,
               // kc == null &&  (kc = comparableClassFor(k)) == null  如果插入的元素x没有实现Comparable接口则通过tieBreakOrder去比较
               // (dir = compareComparables(kc, k, pk)) == 0 如果实现了Comparable接口,p节点为空或x元素不是同一个类,也是通过tieBreakOrder去比较,否则通过compareComparables里面的compareTo方法去比较,如果也为0,则也是通过tieBreakOrder去比较,否则直接返回该结果
               else if ((kc == null &&
                         (kc = comparableClassFor(k)) == null) ||
                        (dir = compareComparables(kc, k, pk)) == 0)
                   dir = tieBreakOrder(k, pk);
				// 用xp来指向当前遍历红黑树的节点p
               TreeNode<K,V> xp = p;
               // p根据dir的值去选择指向其左右节点,并判断其是否为空,如果不为空循环下一次
               // 换句话说就是你要插入的位置是否有值了,有值就继续循环
               if ((p = (dir <= 0) ? p.left : p.right) == null) {
               		// 设置要插入元素x在红黑树中的父节点是xp(就是红黑树遍历的当前元素) 
                   x.parent = xp;
                   // 指定插入到左边还是右边。
                   if (dir <= 0)
                       xp.left = x;
                   else
                       xp.right = x;
                   // 到此为止元素已经插入到对应的树中,最后就是将数转换成红黑树了
                   root = balanceInsertion(root, x);
                   break;
               }
           }
       }
   }
   // 确保转化后的红黑树在数组上
   moveRootToFront(tab, root);
}
// 将数转化成为红黑树,主要是红黑树的实现规则,可以大概了解下
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    // 将插入的元素x置为红色
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
    	// 如果x的父节点是空的,将x颜色置为黑色,返回(其实就是红黑树的特点,根节点为黑色)
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // 如果x的父节点是黑色或者爷爷节点是空的,则就是红黑树直接返回了
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 如果父节点是爷爷节点的左节点
        if (xp == (xppl = xpp.left)) {
        	// 如果爷爷节点的右节点不为空且是红色,则爷爷节点的右置为黑色,父节点也置为黑色,爷爷节点置为红色,
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                // 再次循环调整数,此时起点不再是插入的元素x,而是从xpp开始调整了
                x = xpp;
            }
            else {
            	// 如果爷爷节点的右节点为空或是黑节点
            	// 如果当前插入元素是父节点的右节点
                if (x == xp.right) {
                	// 针对父节点进行左旋,想了解怎么实现可以自己跟下代码
                    root = rotateLeft(root, x = xp);
                    // 获取爷爷节点
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 如果父节点不为空
                if (xp != null) {
                	// 父节点设为黑色
                    xp.red = false;
                    //如果爷爷节点不为空
                    if (xpp != null) {
                    	// 则爷爷节点置为红色
                        xpp.red = true;
                        // 针对爷爷节点进行右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 如果父节点是爷爷节点的右节点
        else {
        	// 如果爷爷节点的左节点不为空且为红色,则爷爷节点的左节点置为黑色,父节点也置为黑色,爷爷节点置为红色,
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                // 再次循环调整数,此时起点不再是插入的元素x,而是从xpp开始调整了
                x = xpp;
            }
            else {
                // 如果爷爷节点的左节点为空或是黑节点
            	// 如果当前插入元素是父节点的左节点
                if (x == xp.left) {
                	// 针对父节点进行右旋
                    root = rotateRight(root, x = xp);
                    // 获取爷爷节点
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 如果父节点不为空
                if (xp != null) {
                	// 父节点置为黑色
                    xp.red = false;
                    // 如果爷爷节点不为空
                    if (xpp != null) {
                    	// 爷爷节点置为红色
                        xpp.red = true;
                        // 针对爷爷几点进行左旋
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}
// 主要是确保转化的红黑树还在数组正确位置上
// 结合上面方法我们知道了数组上的链表既是红黑树,也是双向链表
 static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
    	// 获取根元素的下标在数组里面的位置
        int index = (n - 1) & root.hash;
        // 获取该数组位置上的元素first
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        // 如果该first不是根元素,则需要处理了
        if (root != first) {
            Node<K,V> rn;
            // 将数组该位置设置成root
            tab[index] = root;
            // 获取根节点的前一个节点
            TreeNode<K,V> rp = root.prev;
            // root的后一个节点不为空
            if ((rn = root.next) != null)
            	// 则摘除双向链表的root节点
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
        		// 如果first不为空,则first作为root的下一个元素
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        // 校验是否满足红黑树和双向链表的特性
        assert checkInvariants(root);
    }
}
// 对接前面,如果插入元素的key的hash值与树节点的key的hash值相同时获取dir的条件,该方法就是判断插入元素是否有实现Comparable接口
static Class<?> comparableClassFor(Object x) {
	// 如果插入元素的key是Comparable的实例(实现了Comparable接口,范围有点大,后面继续作限制)
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        // 如果对象是String,直接返回该对象的类对象
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
         // getGenericInterfaces():返回的是该对象的运行时类型直接实现的接口数组,不能是继承的
        if ((ts = c.getGenericInterfaces()) != null) {

           /*
            * 五个条件同时满足时,才获取到正确的类型
            * 1、((t = ts[i]) instanceof ParameterizedType):实现了泛型参数的类型
            * 2、((p = (ParameterizedType) t).getRawType() == Comparable.class):获取接口不带参数部分的类型对象且该类型是Comparable
            * 3、(as = p.getActualTypeArguments()):获取泛型参数数组
            * 4、as.length == 1:只有一个泛型参数,
            * 5、as[0] == c:该实现类型是该类型本身
            * 总结:就是key要实现jdk提供的public interface Comparable<T>接口,用这些条件去限制,放在实现自己写的
            */
            for (int i = 0; i < ts.length; ++i) {
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    return null;
}

// 如果节点p是null,或者p和x不是一个类对象,则返回0,否则返回实现的compareTo方法的值
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}
// 当插入元素x与树结点p的键的hash值相同时进行比较a代表插入元素的key,b代表树结点的key
// 如果a为空或b为空或者通过String的compareTo方法比较的类名相等,则比较的a和b默认hashcode的值
// 否则就是用String的compareTo方法比较的类名结果
 static int tieBreakOrder(Object a, Object b) {
     int d;
     if (a == null || b == null ||
         (d = a.getClass().getName().
          compareTo(b.getClass().getName())) == 0)
          // System.identityHashCode(a): 该方法是无论类是否有覆盖hashCode方法,都会调用默认的hashCode方法去返回值
         d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
              -1 : 1);
     return d;
 }

2.链表转红黑树步骤总结

链表转化成红黑树的实现步骤分为以下几步
  1. 当链表结点达到8时调用此方法进行转换
  2. 判断数组长度是否达到64,有则转化,没有则扩容
  3. 将原单向链表转化成双向链表,再将双向链表放到数组原来位置上
  4. 遍历链表去进行转换,每次转换都会从根节点遍历红黑树,确定元素放在红黑树的位置,插入上去后调整树为红黑树
  5. 当所有元素转换完成后将转换的红黑树放在数组原位置上
确定元素放在红黑树的位置
  1. 确定树是否为空,为空则放到根目录上
  2. 比较存放元素与当前遍历树的结点的的hash值,当前数结点的hash值大于存放元素的hash值,会将元素存放在左结点,前数结点的hash值小于存放元素的hash值,会将元素存放到右结点;当两则相等时比较其他大小确定
  3. 判断左节点或右结点是否为空,为则放置到该位置,否则继续遍历树
插入元素和树结点的值一样时的比较方式(画的有点丑)

在这里插入图片描述

3.红黑树转链表的代码实现

// 实现方式非常简单,转化成红黑树后,遍历链表,转化成单向链表,返回表头
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

4.其他关于hashmap的分析

hashmap的常见静态属性和方法
hashmap的扩容机制
hashmap的存放元素的实现过程

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

hashmap链表转化成红黑树的过程以及红黑树转化成链表的过程 的相关文章

  • linux与windows文件共享及全屏

    1 安装VMware tools 虚拟机选项 安装VMware tools 然后在Ubuntu系统中弹出的VMware tools窗口中 找到VMwaretools 9 6 0 1294478 tar gz 复制到桌面 然后在桌面上的VMw

随机推荐

  • [python]windos下打包一个简单的python脚本

    1 脚本一览 python脚本如下 结构比较简单 基本功能是根据已有的名单去统计群里面没有参加接龙的人员和人数 脚本只涉及到python自带的库 并且在运行时需要读取同目录下的两个txt文件 最后打印出没有统计结果 import os st
  • 微信营销系统如何使用效果会更好

    微信作为中国最大的社交平台之一 已经成为企业私域营销的重要阵地 在这个庞大的社交网络中 如何使用微信营销系统 将直接影响到企业的营销效果 本文将深入探讨如何更好地利用微信营销系统 以实现更好的私域营销效果 1 确定营销目标和策略 在使用微信
  • Java手写ArrayList和拓展案例

    Java手写ArrayList 思维导图 mermaid svg ncipf5YgQSnFk37I font family trebuchet ms verdana arial sans serif font size 16px fill
  • 没遇到过这三个问题都不好意思说用过Redis

    缓存是互联网应用中不可或缺的一部分 而提到缓存 就不得不提它的三个经典问题 缓存穿透 缓存击穿和缓存雪崩 我称它们为缓存问题三兄弟 缓存的作用主要有两个 一来提升访问速度 二来保护数据库 在业务量不大的时候 通常没什么大问题 但当业务量起来
  • C语言-->调用函数计算学生成绩平均分、课程平均分等。

    这是一道c语言的课后题 题目如下 输入10个学生5分课程的成绩 分别用函数实现下列功能 1 计算每个学生的平均分 2 计算每门课的平均分 3 找出所有50个分数中最高的分数对应的学生和课程 4 计算平均分方差 十个学生五门课的成绩 用函数实
  • 实现数组的逆序存放

    今天分享给大家一个实现逆序输出数组的程序 咱们先上代码 define CRT SECURE NO WARNINGS 1 include
  • Android 9.0 根据包名清理应用数据

    1 前言 在9 0的系统ROM定制化开发中 在系统原生设置中 可以在app的详情页里面看到清理缓存 清理数据等选项 而在最近的rom产品定制化中 有产品需求要求 在第三方app 的接口中 调用接口来实现清除app里面的数据 在Activit
  • 常见状态码 【最全状态码展示】

    一 什么是状态码 HTTP状态码 HTTP Status Code 是用以表示网页服务器HTTP响应状态的3位数字代码 它由 RFC2616 规范定义的 并得到RFC 2518 RFC 2817 RFC 2295 RFC 2774 RFC
  • C语言 字符指针的定义与初始化

    1 字符指针定义说明 指向字符串的指针称为字符指针 其定义形式为 char 指针名 在定义字符指针的同时为其赋值称为字符指针的初始化 如 void main char p Hello printf s p 定义一个字符指针p 并使指针p得到
  • 如何用最通俗易懂的方式理解假设检验

    https blog csdn net wydyd110 article details 82387653
  • 日本半导体制造商AKM工厂失火停产,市场再次掀起抢货潮!

    数据猿年度重磅活动预告 2020年度金猿策划活动 金猿榜单发布 金猿奖杯颁发 即将推出 尽情咨询期待 大数据产业创新服务媒体 聚焦数据 改变商业 据日本共同社报道 10月20日 旭化成旗下集团公司从事半导体制造的旭化成微电子株式会社 简称
  • C++从0到1(2):数据类型

    目录 1 整型 2 sizeof关键字 3 实型 浮点型 4 字符型 5 转义字符 6 字符串型 7 布尔类型 8 cin 数据的输入 C 规定在创建一个变量或常量时 必须要指定相应的数据类型 否则无法给变量分配内存 数据类型存在的意义 给
  • 富文本编辑器的使用方法

    富文本编辑器又称Rich Text Editor 简称RTE 它不同与文本编辑器 程序员可以到网上下载免费的富文本编辑器嵌于自己设计的网站或者程序里 方便用户编辑文章或者信息 主要用于发新闻类似的东西 它有着和word文档还有网上发论坛插图
  • ssd测试mAP的时候出现tensorflow版本问题,问题 _variable_v2_call() got an unexpected keyword argument ‘collections’

    这个问题是Tensorflow 版本太高导致的 我原来使用的 1 13 1 的版本不行 换成了 1 10 1就可以了
  • 2023年新能源汽车行业研究报告

    第一章 行业概况 新能源汽车 是指采用新型动力系统 完全或者主要依靠新型能源驱动的汽车 包括纯电动汽车 插电式混合动力汽车 增程式混合动力汽车和燃料电池汽车等 国际上 混合动力汽车 含中混 强混 插电式混动 汽车 天然气汽车 纯电动汽车和燃
  • 《一周搞定模电》—基本放大电路

    文章目录 TOC 文章目录 一 三极管放大电路 1 饱和失真和截至失真 2 静态工作点 二 放大电路改进 分压偏置电路 一 三极管放大电路 下图是共发射极放大电路 R8两端的电压值与输入信号是反向关系 仿真图如下所示 1 饱和失真和截至失真
  • MySQL大小写敏感的解决方案

    不同的MySQL版本有不同的默认设定 具体情况需要具体分析 mysql是通过lower case table names参数来控制大小写敏感的 该参数在 mysqld 节点下 具体的含义笔者从官网截了一张图 关于lower case tab
  • 算法:链表数字相加

    算法 链表数字相加 问题 解决 问题 解决 class Solution def mergeNodes self head Optional ListNode gt Optional ListNode init re ListNode 0
  • 项目总结@Repository注解dao层接口扫描不到

    使用 Repository来注解 来注解dao层接口 运行运行项目不能扫描 应该是接触的项目比较少 第一次遇到这种情况 使用 Repository注解mapper接口发现项目运行找不到dao层的东西 我滴个神 以前用着这玩意不是挺好使的嘛
  • hashmap链表转化成红黑树的过程以及红黑树转化成链表的过程

    1 链表转红黑树的实现代码 该方法主要是将单向链表转化成双向链表 为了后面操作 比如在后面将红黑树放到数组上时 以及红黑树转成链表时简化操作 final void treeifyBin Node