手写HashMap中的红黑树


前言

java8的HashMap中,使用了红黑树,本文主要是通过手写红黑树插入和查找代码来理解其特性和作用。


一、红黑树是什么?

红黑树是一种数据结构,如果学过数据结构的同学,应该会比较了解,红黑树是一种平衡二叉树,是有234树转变而来。没学过的同学,只需要知道以下两点:

  • 作用:有序且规则的结构,在HashMap中主要是方便查找元素
  • 消耗:插入和删除的消耗相对较少(比平衡二叉树少,比链表多),查找的消耗相对较少(比链表少,比平衡二叉树多)。是一种折中或综合性的策略。

二、代码实现

1.构建存放键值对的节点类

  /**
  * 需要一个Node类来存放KV,且该Node可以指向下个Node(链表结构)
  * 这里直接实现了Map.Entry接口,也可以自己手动写一个Entry接口
  * 
  * @param <K> 键
  * @param <V> 值
  */
  class Node<K, V> implements Map.Entry<K, V>{
    Node<K, V> next;
    final K key;
    V value;
    int hash;

    public Node(Node<K, V> next, K key, V value, int hash) {
      this.next = next;
      this.key = key;
      this.value = value;
      this.hash = hash;
    }

    @Override
    public K getKey() {
      return this.key;
    }

    @Override
    public V getValue() {
      return this.value;
    }

    @Override
    public V setValue(V value) {
      return this.value=value;
    }

    @Override
    public boolean equals(Object obj) {
      return (obj instanceof Node) && 
      ((Node)obj).key.equals(this.key) && 
      ((Node)obj).value.equals(this.value);
    }
  }

2.构建树节点类

继承Node,创建红黑树节点。代码如下(示例):

/**
   * 红黑树节点:
   * 有颜色red
   * 有左右叶子结点 left、right
   * 有父节点 superior
   * @param <K>
   * @param <V>
   */
  class TreeNode<K,V> extends Node<K,V>{
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> superior;
    boolean red;

    public TreeNode(Node<K, V> node){super(node.next,node.key,node.value,node.hash);}
  }

3. 插入方法

/**
   * 该方法包含递归,从根节点开始,每次比对大小进行分路(左右),有空位即临时插入。
   * 然后进行红黑树平衡操作
   *
   * @param superior 需要比较大小父节点
   * @param newTNode 新插入的节点
   * @return 返回平衡操作后的节点
   */
  private TreeNode<K, V> insertTreeNode(TreeNode<K,V> superior, TreeNode<K,V> newTNode) {
    newTNode.red = true;
    boolean left;//插入方向
    if(compareTreeNode(superior.key,newTNode.key)){
      logger.trace("节点"+newTNode.key+"小于节点"+superior.key+",向左子树移动");
      if(superior.left == null){
        superior.left = newTNode;
        newTNode.superior = superior;
        logger.trace("节点"+newTNode.key+"临时插入"+superior.key+"的左子节点");
      }else{
        TreeNode<K,V> tempGrand = superior.superior;
        superior = insertTreeNode(superior.left,newTNode);
        superior.superior = tempGrand;
        if(tempGrand != null) {
          tempGrand.left = superior;
        }
        newTNode = superior.left;
      }

      left = true;
    }else{
      logger.trace("节点"+newTNode.key+"大于节点"+superior.key+",向右子树移动");
      if(superior.right == null){
        superior.right = newTNode;
        newTNode.superior = superior;
        logger.trace("节点"+newTNode.key+"临时插入"+superior.key+"的右子节点");
      }else{
        TreeNode<K,V> tempGrand = superior.superior;
        superior = insertTreeNode(superior.right,newTNode);
        superior.superior = tempGrand;
        if(tempGrand != null)
          tempGrand.right = superior;
        newTNode = superior.right;
      }

      left = false;
    }


    return balanceRBTree(superior, newTNode, left);
  }

4.红黑树平衡

黑红树的平衡实际上每次只对三层内的节点操作,如下图
1、
在这里插入图片描述
2、在这里插入图片描述
3、在这里插入图片描述
4、在这里插入图片描述

代码如下:

/**
   * 对三代节点做操作,保持红黑树平衡
   * 
   * 红黑树平衡操作规律:
   * 插入a节点
   * <p>1.若插入后父节点是红色,且有红色叔节点,则爷节点变红,父叔节点变黑,若爷节点是根节点,变黑</p>
   * <p>2.若操作后父节点是红色,且没有红色叔节点,且a和父节点同向,则父和爷节点左/右旋,互换颜色</p>
   * <p>3.若操作后父节点是红色,且没有红色叔节点,且a和父节点反向,则先a和父节点左/右旋,再父和爷节点左/右旋,互换颜色</p>
   * <p>其他直接插入</p>
   * 
   * @param superior
   * @param newTNode
   * @param left
   * @return
   */
  private TreeNode<K, V> balanceRBTree(TreeNode<K, V> superior, TreeNode<K, V> newTNode, boolean left) {

    TreeNode<K, V> grand,lUncle,rUncle,temp;
    grand = superior.superior;

    if(grand != null) {
      lUncle = superior.superior.left;
      rUncle = superior.superior.right;
      //若操作后父节点是红色
      if(superior.red && newTNode.red){
        //若有红叔节点
        if(lUncle != null && rUncle != null && lUncle.red && rUncle.red){
          blackDown(grand,lUncle,rUncle);
          logger.trace("节点"+rUncle.key+"和"+lUncle.key+"变黑,"+grand.key+"节点变红");
        }else {
          if (superior != (left ? superior.superior.left : superior.superior.right)) {//父节点是反向节点
            //向父节点的方向旋
            logger.trace("节点" + superior.key + (left ? "右旋" : "左旋"));
            superior = left ? rotateRight(superior, newTNode) : rotateLeft(superior, newTNode);
            //父爷节点再旋回来,互换颜色
            logger.trace("节点" + grand.key + (left ? "左旋" : "右旋"));
            temp = grand;//假设superior——>a地址内存,temp,grand——>b地址内存
            grand = left ? rotateLeft(grand, superior) : rotateRight(grand, superior);//grand——>a地址内存
            grand.red = false;
            temp.red = true;//temp依旧指向——>b地址内存
            logger.trace("节点" + temp.key + "变红,节点"+grand.key+"变黑");
          } else {//父子节点方向相同
            //父爷节点左旋,互换颜色
            logger.trace("节点" + grand.key + (left ? "右旋" : "左旋"));
            temp = grand;
            grand = left ? rotateRight(grand, superior) : rotateLeft(grand, superior);
            grand.red = false;
            temp.red = true;//temp依旧指向——>b地址内存
            logger.trace("节点" + temp.key + "变红,节点"+grand.key+"变黑");
          }
        }
      }
      return grand;
    }else{
      //根节点变黑
      superior.red = false;
      logger.trace("返回根节点(黑色):" + superior.key);
      return superior;
    }
  }

5.左旋、右旋和交换颜色

/**
   * 左旋,右子节点升为父节点,原父节点变成左子节点。原爷节点变为右节点的父节点
   * 若原右节点的左子节点变成原父节点的右子节点。
   * @param superior
   * @param right
   * @return
   */
  private TreeNode<K,V> rotateLeft(TreeNode<K,V> superior, TreeNode<K,V> right) {
    TreeNode<K,V> tempLeft = right.left;
    right.left=superior;
    right.superior = superior.superior;
    superior.right = tempLeft;
    superior.superior = right;
    if(tempLeft != null)
      tempLeft.superior = superior;
    return right;
  }

  /**
   * 右旋,参考左旋
   * @param superior
   * @param left
   * @return
   */
  private TreeNode<K,V> rotateRight(TreeNode<K,V> superior, TreeNode left) {
    TreeNode<K,V> tempRight = left.right;
    left.right=superior;
    left.superior = superior.superior;
    superior.left = tempRight;
    superior.superior = left;
    if(tempRight != null)
      tempRight.superior = superior;
    return left;
  }

  /**
   * 交换颜色
   * 
   * 父节点变红,子节点变黑
   * @param grand
   * @param lUncle
   * @param rUncle
   */
  private void blackDown(TreeNode<K,V> grand, TreeNode<K,V> lUncle, TreeNode<K,V> rUncle) {
    grand.red = true;
    lUncle.red = false;
    rUncle.red = false;
  }

8.测试验证

实际上我做了大量的单元测试,抽其中一段代码展示

  @Test
  void insertTreeNode_6() throws Exception {
  	//反射私有方法
	testInsertTreeNode = 
		MyHashMap.class.getDeclaredMethod("insertTreeNode", MyHashMap.TreeNode.class, MyHashMap.TreeNode.class);
    testInsertTreeNode.setAccessible(true);
    //创建节点
    node21 = myHashMap.new<String,String> Node<String,String>(null,"21","21",(int) testHash.invoke(myHashMap,"1"));
    node31 = myHashMap.new<String,String> Node<String,String>(null,"31","31",(int) testHash.invoke(myHashMap,"21"));
    node11 = myHashMap.new<String,String> Node<String,String>(null,"11","11",(int) testHash.invoke(myHashMap,"22"));
    node10 = myHashMap.new<String,String> Node<String,String>(null,"10","10",(int) testHash.invoke(myHashMap,"22"));
    node12 = myHashMap.new<String,String> Node<String,String>(null,"12","12",(int) testHash.invoke(myHashMap,"22"));
    node32 = myHashMap.new<String,String> Node<String,String>(null,"32","32",(int) testHash.invoke(myHashMap,"31"));
    node22 = myHashMap.new<String,String> Node<String,String>(null,"22","22",(int) testHash.invoke(myHashMap,"32"));
    node33 = myHashMap.new<String,String> Node<String,String>(null,"33","33",(int) testHash.invoke(myHashMap,"32"));
    node30 = myHashMap.new<String,String> Node<String,String>(null,"30","30",(int) testHash.invoke(myHashMap,"32"));

    t21 = myHashMap.new<String,String> TreeNode<String,String>(node21);
    t31 = myHashMap.new<String,String> TreeNode<String,String>(node31);
    t11 = myHashMap.new<String,String> TreeNode<String,String>(node11);
    t10 = myHashMap.new<String,String> TreeNode<String,String>(node10);
    t12 = myHashMap.new<String,String> TreeNode<String,String>(node12);
    t22 = myHashMap.new<String,String> TreeNode<String,String>(node22);
    t32 = myHashMap.new<String,String> TreeNode<String,String>(node32);
    t33 = myHashMap.new<String,String> TreeNode<String,String>(node33);
    t30 = myHashMap.new<String,String> TreeNode<String,String>(node30);
    //构造树
    /*
            黑11
           /  \
        黑10  红21
              /  \
           黑12	 黑30
                / \
            红22  红31

     */
    t11.left = t10;
    t11.right = t21;
    t11.red = false;

    t10.superior = t11;
    t10.red = false;

    t21.superior = t11;
    t21.red = true;
    t21.left=t12;
    t21.right=t30;

    t12.superior=t21;
    t12.red=false;

    t30.superior=t21;
    t30.left=t22;
    t30.right=t31;
    t30.red=false;

    t22.superior=t30;
    t22.red=true;

    t31.superior=t30;
    t31.red=true;
    /*插入32节点:
               黑11                       黑11                    黑21
               /  \                       /  \                 	/    \
        	黑10  红21                  黑10  红21            红11  	 红30
                  /  \      ————>         	 /  \    ————>  /  \     /	\
            	黑12 黑30                  黑12	黑30      黑10 黑12 黑22 黑31
    		         /  \                  	     / \                 	  \
    		       红22 红31                   红22 红31                   红32
                                                     \
                                                     红32
   */
    MyHashMap<String,String>.TreeNode<String,String> top  = (MyHashMap<String, String>.TreeNode<String, String>) testInsertTreeNode.invoke(myHashMap, t11, t32);

    assertAll("红黑树平衡测试:",
            ()->assertEquals(t21.key,top.key),
            ()->assertEquals(false,t21.red),
            ()->assertEquals(t11.key, top.left.key),
            ()->assertEquals(true,top.left.red),
            ()->assertEquals(t10.key, top.left.left.key),
            ()->assertEquals(false,top.left.left.red),
            ()->assertEquals(t12.key, top.left.right.key),
            ()->assertEquals(false,top.left.right.red),

            ()->assertEquals(t30.key, top.right.key),
            ()->assertEquals(true,top.right.red),
            ()->assertEquals(t22.key, top.right.left.key),
            ()->assertEquals(false,top.right.left.red),
            ()->assertEquals(t31.key, top.right.right.key),
            ()->assertEquals(false,top.right.right.red),
            ()->assertEquals(t32.key, top.right.right.right.key),
            ()->assertEquals(true ,top.right.right.right.red),

            ()->assertNull(top.left.left.left),
            ()->assertNull(top.left.left.right),
            ()->assertNull(top.left.right.left),
            ()->assertNull(top.left.right.right),
            ()->assertNull(top.right.left.left),
            ()->assertNull(top.right.left.right),
            ()->assertNull(top.right.right.left),
            ()->assertNull(top.right.right.right.left),
            ()->assertNull(top.right.right.right.right)
    );
  }

测试结果为绿色通过。


总结

文中为了方便理解和阅读,代码中大量使用了递归,递归的效率会比使用迭代差。实际上源码主要是用的迭代方式处理,而且还包含了删除等其他操作,相比较更为复杂。

### 手动实现 HashMap 的代码及注意事项 #### 1. **核心原理概述** `HashMap` 是基于哈希表的数据结构,其主要功能是以键值对的形式存储数据。内部通过数组和链表(或红黑树)相结合的方式解决哈希冲突问题[^4]。以下是其实现的关键点: - 哈希函数用于计算键的索引位置。 - 当发生哈希碰撞时,采用拉链法(链地址法)或者红黑树优化存储相同索引的节点。 #### 2. **自定义 HashMap 实现** 以下是一个简化版的手写 `HashMap` 示例,展示了基本的功能逻辑: ```java public class MyHashMap<K, V> { // 默认初始容量 private static final int DEFAULT_CAPACITY = 16; // 加载因子阈值 private static final float LOAD_FACTOR_THRESHOLD = 0.75f; // 存储桶数组 private Entry<K, V>[] table; // 键值对的数量 private int size; @SuppressWarnings("unchecked") public MyHashMap() { table = new Entry[DEFAULT_CAPACITY]; size = 0; } // 内部静态类表示键值对条目 private static class Entry<K, V> { K key; V value; Entry<K, V> next; // 链表指针 public Entry(K key, V value, Entry<K, V> next) { this.key = key; this.value = value; this.next = next; } } // 计算哈希值 private int hash(K key) { return Math.abs(key.hashCode()) % table.length; } // 插入/更新操作 public void put(K key, V value) { if (key == null) throw new IllegalArgumentException("Key cannot be null."); int index = hash(key); Entry<K, V> entry = table[index]; while (entry != null) { if (entry.key.equals(key)) { entry.value = value; // 更新已有值 return; } entry = entry.next; } // 添加新节点到头部 table[index] = new Entry<>(key, value, table[index]); size++; // 判断是否需要扩容 if ((float)size / table.length >= LOAD_FACTOR_THRESHOLD) { resize(); } } // 获取操作 public V get(K key) { if (key == null) return null; int index = hash(key); Entry<K, V> entry = table[index]; while (entry != null) { if (entry.key.equals(key)) { return entry.value; } entry = entry.next; } return null; // 若找不到对应键返回null } // 删除操作 public V remove(K key) { if (key == null || isEmpty()) return null; int index = hash(key); Entry<K, V> prev = null; Entry<K, V> curr = table[index]; while (curr != null && !curr.key.equals(key)) { prev = curr; curr = curr.next; } if (curr == null) return null; // 没找到对应的键 if (prev == null) { // 删除头结点的情况 table[index] = curr.next; } else { prev.next = curr.next; } size--; return curr.value; } // 扩容方法 @SuppressWarnings("unchecked") private void resize() { Entry<K, V>[] oldTable = table; int newSize = oldTable.length * 2; table = new Entry[newSize]; size = 0; for (Entry<K, V> entry : oldTable) { while (entry != null) { put(entry.key, entry.value); // 重新分配到新的桶中 entry = entry.next; } } } // 是否为空判断 public boolean isEmpty() { return size == 0; } // 返回当前大小 public int size() { return size; } } ``` --- #### 3. **实现中的注意事项** ##### (1)**哈希函数的设计** 良好的哈希函数应尽量减少冲突概率,通常可以通过重写对象的 `hashCode()` 方法来改善分布均匀性[^4]。在实际项目中,建议依赖 JDK 自带的 `Object.hashCode()` 或者第三方库提供的更高效的散列算法。 ##### (2)**负载因子与动态调整** 默认情况下,当填充率达到约 75% 时会触发扩容动作以维持性能稳定。过低的负载因子会导致浪费大量空间;过高则可能增加查找成本。因此合理设置这一参数至关重要[^5]。 ##### (3)**线程安全性考量** 正如之前提到过的那样,原始形态下的 `MyHashMap` 并不具备天然的同步特性,在多线程环境下使用前必须额外加以防护措施,比如借助外部锁机制或是替换为更适合并发场景的产品级容器如 `ConcurrentHashMap`[^3]。 ##### (4)**处理哈希冲突的方法选择** 本例采用了较为基础的链地址法应对重复映射情形。然而随着规模增大,这种方法可能会退化成 O(n) 时间复杂度的操作序列。针对这种情况,现代 JVM 中引入了红黑树转换策略——即当某个槽位上的链条长度超过一定界限时便将其重构为平衡二叉搜索从而提升检索效率[^6]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值