数据结构之手动实现HashMap

本文详细解析了HashMap的内部结构,包括其“数组+链表”的数据结构特点,以及Node类的属性与作用。探讨了HashMap的核心操作,如添加、获取和删除元素的过程,同时解释了扩容机制和哈希碰撞的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前两篇博文
数据结构之手动实现ArrayList
数据结构之手动实现LinkedList
分别介绍了List接口的两个子类的优缺点及底层的数据结构,本篇文章将介绍一下数据结构为“数组+链表”的HashMap。
一、HashMap核心结构
查看HashMap源代码可以发现两大核心:

transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

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

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

上述代码中,Node<K,V>[] table 数据便是核心,即“位桶”数组,在位桶数组中存放Node<K,V>这样的节点对象,Node对象存储了
1、hash:键对象的hash值
2、key:键对象
3、value:值对象
4、next:下一个节点
显然,由一个个Node<K,V>对象组成了单向链表,就像这样
在这里插入图片描述
table的结构如下
在这里插入图片描述
存储过程如下
在这里插入图片描述
二、代码实现
以下只是实现了核心方法

public class Node2<K,V> {
    //hash值
    int hash;
    K key;
    V value;
    //下一个节点对象,由于是单链表,只定义指向下一个对象的指针
    Node2 next;

    public Node2() {}
}
public class HashMapTest<K,V> {
    //定义位桶数组
    Node2[] table;
    //存放的键值对的个数
    int size;
    public HashMapTest(){
        table = new Node2[16];//位桶数组的长度一般定义为2的整数幂
    }

    /**
     * 添加元素
     * @param key
     * @param value
     */
    public void put(K key,V value){
        //考虑数组的扩容更加完善,源码中有个加载因子,为0.75f,还有个临界值,临界值=table.length*加载因子,当数组长度大于临界值时,就会触发扩容操作。

        //构造节点对象
        Node2 node = new Node2();
        node.hash = myHashValue(key.hashCode(),table.length);
        node.key = key;
        node.value = value;
        node.next = null;

        //看看它往数组的那个位置存??
        Node2 temp = table[node.hash];

        //正在遍历的最后一个元素
        Node2 iterLast = null;
        //是否重复的标志
        boolean keyRepeat = false;
        //数组该位置位空
        if(temp==null){
            //数组为空直接将节点对象放进去
            table[node.hash] = node;
            size++;
        }else{
            //数组不为空,则遍历链表
            while(temp!=null){
                if(key.equals(temp.key)){
                    //如果key重复,则覆盖
                    temp.value = value;
                    keyRepeat = true;
                    //停止循环,无需找到最后的节点对象
                    break;
                }else{
                    //key不重复,则遍历下一个
                    iterLast = temp;
                    temp = temp.next;
                }
            }
            //没有发生Key重复的情况,则添加到链表的最后
            if(!keyRepeat){
                iterLast.next = node;
                size++;
            }
        }
    }

    /**
     * 根据键获取值
     * @param key
     * @return
     */
    public V get(K key){
        V value = null;
        int hash = myHashValue(key.hashCode(),table.length);
        Node2 temp = table[hash];
        if(temp!=null){
            while(temp!=null){
                if(key.equals(temp.key)){
                    value = (V)temp.value;
                    break;
                }else{
                    temp = temp.next;
                }
            }
        }
        return value;
    }

    /**
     * 根据键删除键值对
     * @param key
     */
    public void remove(K key){
        int hash = myHashValue(key.hashCode(),table.length);
        //prev是该索引位置的链表的第一个节点
        Node2 prev = table[hash];
        Node2 e = prev;
        //开始遍历链表
        while(e != null){
            Node2 next = e.next;
            Object k;
            //找到了给定key的节点对象
            if(e.hash == hash && ((k = e.key)==key || (key != null && key.equals(k)))){
                if(prev == e){
                    table[hash] = next;
                }else{
                    prev.next = next;
                }
                size--;
            }
            prev = e;
            e = next;
        }
    }

    private int myHashValue(int value,int length){
        return value&(length-1);//位运算,效率高  value%(length-1) 取模,效率低
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("{");
        //首先遍历位桶数组
        for(int i = 0; i < table.length; i ++){
            //其次遍历数组每个下标位置的链表
            //获取头结点
            Node2 temp = table[i];
            while(temp!=null){
                sb.append(temp.key+":"+temp.value+",");
                temp = temp.next;
            }
        }
        sb.setCharAt(sb.length()-1,'}');
        return sb.toString();
    }

    public static void main(String[] args){
        HashMapTest<Integer,String> map = new HashMapTest<>();
        map.put(10, "aa");
        map.put(20, "bb");
        map.put(30, "cc");
        map.put(20, "ssss");
        map.put(53, "gg");
        map.put(69, "hh");
        map.put(85, "kk");
        System.out.println(map);
        map.remove(53);
        System.out.println(map);
    }
}

在此,请注意上面的一个方法

private int myHashValue(int value,int length){
        return value&(length-1);//位运算,效率高  value%(length-1) 取模,效率低
    }

这个方法的主要作用就是为了将key定位到“位桶”数组的某个索引位置,这是一种为了减少哈希碰撞而产生的算法,在Java中,HashMap中定位到位桶数组位置的方法就是根据key的HashCode与数组的长度取模来计算得到的。源码中表示为

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

可以改变为上述代码中的:value&(length-1),即key的hashCode模数组的长度减一。
总结:
当添加一个元素(key-value)时,首先计算key的hash值(注意:此处的hash值不是大家以为的调用HashCode()获取到的int类型的数值),以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。 JDK8中,插入元素的方式由JDK1.7的头插发变为尾插法。当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率,当红黑树的节点数量小于6时,又转换为链表。
扩容问题:
HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小变为原来2倍大小。
扩容很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。
JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值