认真研究HashMap的结点移除

关联博文
数据结构之Map基础入门与详解
认真学习Java集合之HashMap的实现原理
认真研究HashMap的读取和存放操作步骤
认真研究HashMap的初始化和扩容机制
认真研究JDK1.7下HashMap的循环链表和数据丢失问题
认真研究HashMap中的平衡插入
认真研究HashMap中的平衡删除

前面系列博文,我们研究了HashMap的数据结构、get、put操作以及put后的红黑树平衡,本文我们分析HashMap的结点移除。本文基于jdk1.8环境。

我们常用的remove方法有如下两种,其本质都是委派给了removeNode方法。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}


@Override
public boolean remove(Object key, Object value) {
   return removeNode(hash(key), key, value, true, true) != null;
}

【1】removeNode

接下来我们详细分析removeNode方法,其中boolean matchValue表示只有和检测到的value值相等才进行移除。

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //定位到哈希桶中位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;

        // 直接检索到-挺幸运
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
        
        //否则就向后遍历,又区分链表和红黑树
            if (p instanceof TreeNode)
            	//从红黑树中遍历	
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
           
            else {
            	// 链表遍历
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
             // 移除树节点                
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            
            //移除链表结点的两种情况
            else if (node == p)
                tab[index] = node.next;//头结点
            else
                p.next = node.next;//把node割离出来

			//结构调整计数器+1
            ++modCount;
	        // 总数目-1
            --size;
            
            //移除后的操作 linkedhashmap实现了这个方法
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

可以看到整体流程还是很清晰的,这里麻烦在于树结点的查找和移除。

【2】树节点的查找

如下所示,其首先判断当前结点是否为根节点,然后触发find方法。也就是说要保证从根节点开始往下遍历查找。

final TreeNode<K,V> getTreeNode(int h, Object k) {
    return ((parent != null) ? root() : this).find(h, k, null);
}

我们继续看find方法,h表示hash(key),k就是key

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
       
        // 大于h就往左侧找
        if ((ph = p.hash) > h)
            p = pl;
         //小于h就往右侧找   
        else if (ph < h)
            p = pr;
        //key相等--太幸运了直接返回    
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        
        // hash值相等的情况下
        //如果pl为null,就往右侧找   
        else if (pl == null)
            p = pr;
        //如果pr为null,就往左侧找    
        else if (pr == null)
            p = pl;
        // 都不为null,就必须确定是pl 还是pr ,首先使用compareTo进行比较
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
                 //尝试触发compareTo方法 如果k<pk 则p=ol 否则p=pr
            p = (dir < 0) ? pl : pr;
        //迫于无奈往右侧寻找    
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
        // 最后p=pl往左侧寻找
            p = pl;
    } while (p != null);
    return null;
}

comparableClassFor: 如果对象x的类是C,如果C实现了Comparable<C>接口,那么返回C,否则返回null。这个方法的作用就是查看对象x是否实现了Comparable接口

compareComparables:如果x的类型是kc,返回k.compareTo(x)的比较结果。如果x为空,或者类型不是kc,返回0

方法流程梳理如下:

  • 如果ph>h,则将pl赋予p;
  • 如果ph<h,则将pr赋予p;
  • 如果ph==h,且pk==k , 直接返回p;
  • 如果pl == null,则p=pr;从右侧找
  • 如果pr == null,则 p = pl;从左侧找
  • 如果kc不为null,尝试调用其compareTo方法进行比较,如果k<pk 则p=pl 否则p=pr;
  • 最后手段如果q = pr.find(h, k, kc)!=null,返回q;也就是从右侧找
  • 最终手段,直接 p = pl;往左侧找
  • 如果前面没有return,且p不为null,则进行下次循环。否则返回null

总结就是根据hash判断左右,然后根据左右是否为null确定左右。如果尝试使用compareTo得不到左右分支那么就尝试在右侧分支查找,最终去左侧分支查找。

【3】树节点的删除

代码如下所示,首先确定当前哈希桶中索引位置,也就是int index = (n - 1) & hash

  • ① 进行prev、next的处理
  • ② 确定replacement
  • ③ 使用replacement替代掉当前结点
  • ④ 移除后的树平衡
  • ⑤ 将root指定到tab[index]位置。

其中关于第二步确定replacement,如果pl且pr不为null,那么需要找到当前结点p的后继结点交换颜色、交换位置,然后尝试使用后继结点的right替代掉p(如果后继结点的right存在)。

//前置触发
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

//如下hash是当前结点的hash
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    int n;
    if (tab == null || (n = tab.length) == 0)
        return;
     
     // 结点所在桶的位置   
    int index = (n - 1) & hash;

	//确定当前hash位置的头结点first,并赋予root=first
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;

    //记录当前结点的next、prev
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;

	//说明当前结点是头结点,那么直接将next前移
    if (pred == null)
        tab[index] = first = succ;
    else
        pred.next = succ;//node.prev.next = node.next 把node割离出来
    
	//如果next结点存在,node.next.prev=node.prev 把node割离出来
	if (succ != null)
        succ.prev = pred;
// ---------注意,到这里,当前结点node前后 prev next已经改变----------------
    
    //first什么情况下为null?    
    //first为null的情况下无需额外处理,直接返回即可
    if (first == null)
        return;
     
     //确认根节点   
    if (root.parent != null)
        root = root.root();
    
    //转化为链表    
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }

	// p 表示当前结点
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;

-----------------------------------确定replacement----------------------------------
	// 如果pl 、 pr都不为null
    if (pl != null && pr != null) {
        TreeNode<K,V> s = pr, sl;
       
        // s 为右侧子树的最小左孩子,也就是当前结点的后继结点
        while ((sl = s.left) != null) // find successor
            s = sl;

        //交换后继结点与当前结点的颜色 --第一步  
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        
        //sr可能为null, 后继结点的right
        TreeNode<K,V> sr = s.right;
        TreeNode<K,V> pp = p.parent;
        
        //p没有左孩子,只有右孩子,且右孩子没有左孩子, 交换sp
        if (s == pr) { // p was s's direct parent
            p.parent = s;
            s.right = p;
        }
        else {
        // p 与 s 交换位置,此时P还在树结构中哦 -第二步
            TreeNode<K,V> sp = s.parent;
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            if ((s.right = pr) != null)
                pr.parent = s;
        }

		//因为前面s.left为null
        p.left = null;

		//sr的新parent为p
        if ((p.right = sr) != null)
            sr.parent = p;
		//pl的新parent为s
        if ((s.left = pl) != null)
            pl.parent = s;
        
        //s的新parent为pp,如果为null,那么 s 就是root    
        if ((s.parent = pp) == null)
            root = s;
         
         //如果 p 是 pp 的左孩子,那么新的左孩子是s   
        else if (p == pp.left)
            pp.left = s;
        else
            pp.right = s;
        
        // 确定替换结点--也就是哪个结点替换p
        //1.P与后继结点交换;
        //2.“替换结点”替代掉P
        if (sr != null)
            replacement = sr;
        else
            replacement = p;
    }
    // pl不为null,pr为null
    else if (pl != null)
        replacement = pl;
    //    pr不为null,pl为null
    else if (pr != null)
        replacement = pr;
    //     pl=pr=null
    else
        replacement = p;

----------------------------------------------------------------     
     //这一步是用   replacement替换掉 p  --第三步 注意这时 replacement != p
     // replacement 比如sr pl pr
    if (replacement != p) {
        TreeNode<K,V> pp = replacement.parent = p.parent;
        if (pp == null)
            root = replacement;
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;

        // p 从树结构中脱离    
        p.left = p.right = p.parent = null;
    }
	
	//同平衡插入一样,这里有平衡删除,也就是移除结点后进行平衡  --第四步
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

 // 第三步的第二种情况,比如pr pl都为null,或者sr为null,
 // 这一步同样是把p从树结构中割离
    if (replacement == p) {  // detach
        TreeNode<K,V> pp = p.parent; 
        p.parent = null;
        if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    // 把root结点移动到tab[index]位置
    if (movable)
        moveRootToFront(tab, r);
}

最后我们再看一下删除结点后的树的平衡吧。也就是balanceDeletion(root, replacement)方法。关于这个可以参考博文:认真研究HashMap中的平衡删除,本文限于篇幅不再赘述。

### 手动实现哈希表(HashMap)的数据结构和相关算法 以下是基于 Java 编程语言的手写 HashMap 数据结构及其核心方法的完整代码与原理说明: --- #### 1. 基本概念 HashMap 是一种基于键值对存储数据的数据结构,它利用哈希函数将键映射到底层数组的位置。为了应对哈希冲突,通常采用链地址法(即链表或红黑树)。在 JDK 8 及以上版本中,当链表长度达到一定阈值时会自动转换为红黑树以优化性能[^4]。 --- #### 2. 核心组件 - **Node 类**:表示键值对节点。 - **数组**:作为底层容器用于存储 Node 节点。 - **哈希函数**:负责计算键的哈希码并将其映射到数组索引。 - **扩容机制**:当负载因子超过设定值时触发容量翻倍操作。 --- #### 3. 完整代码实现 ```java import java.util.Objects; // 自定义 Node 类 class MyNode<K, V> { final int hash; // 键的哈希值 final K key; // 键 V value; // 值 MyNode<K, V> next; // 链表指针 public MyNode(int hash, K key, V value, MyNode<K, V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } @Override public String toString() { return key + "=" + value; } } // 自定义 HashMap 类 public class MyHashMap<K, V> { private static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量为 16 private static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子为 0.75 private int size; // 当前元素数量 private int threshold; // 阈值 = capacity * loadFactor private final float loadFactor; // 加载因子 private MyNode<K, V>[] table; // 底层数组 // 构造器 public MyHashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public MyHashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Initial Capacity: " + initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load Factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = (int) Math.min(initialCapacity * loadFactor, Integer.MAX_VALUE); this.table = (MyNode<K, V>[]) new MyNode[initialCapacity]; } // 获取指定键对应的值 public V get(K key) { if (key == null) return null; int hash = hash(key); MyNode<K, V> node = getNode(hash, key); return (node != null) ? node.value : null; } // 添加键值对 public void put(K key, V value) { if (key == null) return; int hash = hash(key); MyNode<K, V> newNode = new MyNode<>(hash, key, value, null); addNode(hash, newNode); } // 删除指定键的条目 public V remove(K key) { if (key == null) return null; int hash = hash(key); MyNode<K, V> prev = null; MyNode<K, V> current = table[hash & (table.length - 1)]; while (current != null && !Objects.equals(current.key, key)) { prev = current; current = current.next; } if (current == null) return null; // 未找到对应键 if (prev == null) { // 删除头结点 table[hash & (table.length - 1)] = current.next; } else { // 删除中间/尾部节点 prev.next = current.next; } size--; return current.value; } // 辅助方法:获取指定键所在的节点 private MyNode<K, V> getNode(int hash, K key) { MyNode<K, V> node = table[hash & (table.length - 1)]; while (node != null && !(Objects.equals(node.key, key))) { node = node.next; } return node; } // 辅助方法:添加新节点 private void addNode(int hash, MyNode<K, V> newNode) { if (size >= threshold) resize(); // 判断是否需要扩容 int index = hash & (table.length - 1); // 计算插入位置 MyNode<K, V> head = table[index]; if (head == null) { // 若当前位置为空,直接插入 table[index] = newNode; } else { // 否则加入链表头部 newNode.next = head; table[index] = newNode; } size++; } // 辅助方法:重新调整大小 private void resize() { int oldCap = table.length; int newCap = oldCap << 1; // 新容量加倍 MyNode<K, V>[] oldTable = table; MyNode<K, V>[] newTable = (MyNode<K, V>[]) new MyNode[newCap]; for (int i = 0; i < oldCap; ++i) { MyNode<K, V> node = oldTable[i]; if (node != null) { oldTable[i] = null; // 清理旧表 MyNode<K, V> next; do { next = node.next; int newIndex = node.hash & (newCap - 1); node.next = newTable[newIndex]; // 插入到新表头部 newTable[newIndex] = node; node = next; } while (node != null); } } table = newTable; threshold = (int) (newCap * loadFactor); } // 辅助方法:计算哈希值 private int hash(K key) { int h; return (key == null) ? 0 : ((h = key.hashCode()) ^ (h >>> 16)); } // 输出所有键值对 public void display() { System.out.println("["); for (MyNode<K, V> node : table) { StringBuilder sb = new StringBuilder(); while (node != null) { sb.append(node.toString()).append(", "); node = node.next; } if (!sb.isEmpty()) sb.setLength(sb.length() - 2); // 移除最后一个逗号 System.out.println(sb.toString()); } System.out.println("]"); } } ``` --- #### 4. 关键点解析 - **哈希函数的设计**: 哈希函数的目标是均匀分布键值对,减少哈希冲突的发生概率。上述代码中采用了 `(hashCode() ^ (hashCode() >>> 16)) % length` 的方式来提高低位随机性[^1]。 - **链表转红黑树**: 在 JDK 8 中引入了红黑树的概念,当链表长度超过 8 并且数组长度大于等于 64 时,链表会升级为红黑树以降低时间复杂度[^3]。此功能可以进一步扩展至手写的 HashMap 中。 - **扩容机制**: 当 HashMap 的填充率接近预设的最大值时,会触发扩容操作,即将原数组复制到一个新的两倍大小的数组中,并重新分配所有的键值对[^2]。 --- #### 5. 测试案例 ```java public class Main { public static void main(String[] args) { MyHashMap<String, Integer> map = new MyHashMap<>(); map.put("Alice", 25); map.put("Bob", 30); map.put("Charlie", 35); System.out.println(map.get("Alice")); // 输出 25 System.out.println(map.get("Bob")); // 输出 30 map.remove("Bob"); System.out.println(map.get("Bob")); // 输出 null map.display(); // 显示所有键值对 } } ``` ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值