java实现HashMap和解决Hash碰撞

java实现HashMap和解决Hash碰撞

Hash碰撞(Hash Collision)是指在使用哈希函数时发生的一种情况,即两个不同的输入(例如不同的字符串或文件)经过同一个哈希函数处理后产生了相同的哈希值

Hash碰撞就是两个key经过一系列操作得到了相同的值
请添加图片描述
这篇文章能带你带来什么:

  • 你能了解更深刻的了解Hash碰撞是怎么产生的
  • 你能知道常见的解决Hash碰撞的方式

提取公共类&接口

公共pojo

public class Node<K,V> implements Map.Entry<K,V> {
    private int hash;
    private K key; // 键
    private  V value; // 值
    private Node<K,V> next;
    private int idxOfNext; // 数组索引

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

    public Node(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey()        { return key; }
    public V getValue()      { return 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 int getIdxOfNext() {
        return idxOfNext;
    }

    public void setIdxOfNext(int idxOfNext) {
        this.idxOfNext = idxOfNext;
    }

    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;
    }

    @Override
    public String toString() {
        return "Node{" +
                "hash=" + hash +
                ", key=" + key +
                ", value=" + value +
                ", next=" + next +
                ", idxOfNext=" + idxOfNext +
                '}';
    }
}

Map接口

public interface  MyMap<K,V> {

   /**
    * 插入数据
    * @param key
    * @param value
    */
   void put(K key, V value);

   /**
    * 获取数据
    * @param key
    * @return
    */
   V get(K key);

}

Hash碰撞代码模拟

public class MyHashMap<K, V> implements MyMap<K, V> {
    // 数组用于存储值。初始大小设置为8。
    private Object[] objects = new Object[8];

    /**
     * 将键值对存储到映射中。
     * 如果键已存在,其对应的值将被新值替换。
     *
     * @param key   要存储的键
     * @param value 对应的值
     */
    @Override
    public void put(K key, V value) {
        // 计算键的哈希码并使用位运算确定数组的索引。
        int idx = key.hashCode() & (objects.length - 1);
        // 存储值到计算出的索引位置。
        objects[idx] = value;
    }

    /**
     * 根据给定的键检索值。
     * 如果键不存在,则返回 null。
     *
     * @param key 要检索的键
     * @return 对应的值,如果键不存在则为 null
     */
    @Override
    public V get(K key) {
        // 计算键的哈希码并使用位运算确定数组的索引,然后返回该索引处的值。
        return (V) objects[key.hashCode() & (objects.length - 1)];
    }
}

使用数组&链表解决Hash碰撞

举例

  • 使用数组和链表来解决Hash冲突问题。就像生活有许多个邮箱,每个邮箱都连接着一串个别的信件或包裹(链表)。每个邮箱对应一个哈希索引,而其中的信件代表存储在该索引处的数据。邮箱有不同的大小,信件链长度也各不相同象征着每个索引处的链表如何处理不同数量的数据。

请添加图片描述

public class MyHashMap02BySeparateChaining<K,V> implements MyMap<K, V> {
    // 使用一个数组,每个元素都是一个链表,用于存储具有相同哈希索引的多个节点。
    private LinkedList<Node<K,V>>[] objects = new LinkedList[8];

    /**
     * 将键值对添加到哈希映射中。
     * 如果发生哈希冲突(即多个键有相同的哈希索引),则使用链表存储这些键值对。
     *
     * @param key   要添加的键
     * @param value 对应的值
     */
    @Override
    public void put(K key, V value) {
        // 计算键的哈希码并确定在数组中的索引。
        int idx = key.hashCode() & (objects.length - 1);
        // 如果在该索引位置没有链表,则创建一个新链表并添加节点。
        if (objects[idx] == null) {
            objects[idx] = new LinkedList<>();
            objects[idx].add(new Node<K, V>(key, value));
        } else {
            // 如果链表已存在,则直接在链表末尾添加节点。
            objects[idx].add(new Node<K, V>(key, value));
        }
    }

    /**
     * 根据键检索对应的值。
     * 如果键在哈希映射中不存在,则返回 null。
     *
     * @param key 要检索的键
     * @return 对应的值,如果键不存在则为 null
     */
    @Override
    public V get(K key) {
        // 计算键的哈希码并确定在数组中的索引。
        int idx = key.hashCode() & (objects.length - 1);

        // 遍历链表,查找与给定键相匹配的节点。
        for (Node<K, V> kvNode : objects[idx]) {
            if (key.equals(kvNode.getKey())) {
                // 如果找到匹配的键,返回对应的值。
                return kvNode.getValue();
            }
        }

        // 如果没有找到匹配的键,返回 null。
        return null;
    }
}

开放寻址解决Hash碰撞

当插入新的键值对时,如果计算出的索引位置已经被占用,它会顺序检查后续的索引,直到找到一个空位。同样地,在获取值时,它会从计算出的索引位置开始检查,直到找到匹配的键或遍历完整个数组。这种方法简单高效,适用于键值对数量较少、冲突较少的情况。

举例
开放寻址就像你开车去一个停车场,每个停车位都对应一个哈希表中的槽位。

  1. 哈希冲突:当你按照停车场的某种规则(类似于哈希函数)到达一个特定的停车位时,如果那个位置已经有车了,这就相当于发生了一个“哈希冲突”。

  2. 开放寻址:由于你的首选停车位已被占用,你不会就此放弃,而是开始寻找下一个可用的停车位。这个过程就类似于开放寻址法中的探测序列 —— 你按照一定的顺序检查接下来的停车位。

  3. 解决冲突:最终,当你找到一个空的停车位时,你就会把车停在那里。这个空位就像是哈希表中通过开放寻址法找到的空槽位,用于解决哈希冲突。

请添加图片描述

以下是代码示例

public class MyHashMap03ByOpenAddress<K,V> implements MyMap<K,V> {

    // 使用 Node 类型的数组来存储键值对。
    private Node<K,V>[] objects = new Node[8];

    /**
     * 向哈希映射中插入一个键值对。
     * 如果出现哈希冲突(即计算出的索引位置已被占用),则使用线性探测法找到空闲位置。
     *
     * @param key   要插入的键
     * @param value 对应的值
     */
    @Override
    public void put(K key, V value) {
        // 计算键的哈希码,并根据哈希表大小确定数组索引。
        int idx = key.hashCode() & (objects.length - 1);
        // 如果计算出的索引位置为空,则直接插入新节点。
        if (objects[idx] == null) {
            objects[idx] = new Node<>(key, value);
        } else {
            // 如果该位置已被占用,则循环查找下一个空闲位置。
            while (true) {
                if (objects[idx] == null) {
                    objects[idx] = new Node<>(key, value);
                    break;
                }
                // 线性探测:索引递增,如果到达数组末尾则循环回到数组开始位置。
                idx = (idx + 1) % objects.length;
            }
        }
    }

    /**
     * 根据键从哈希映射中获取值。
     *
     * @param key 要查找的键
     * @return 返回对应的值,如果未找到则返回 null
     */
    @Override
    public V get(K key) {
        // 计算键的哈希码,并根据哈希表大小确定数组索引。
        int idx = key.hashCode() & (objects.length - 1);
        while (true) {
            // 检查当前索引位置的节点是否为要查找的键。
            if (objects[idx] != null && objects[idx].getKey().equals(key)) {
                return objects[idx].getValue();
            }
            // 线性探测:索引递增,如果到达数组末尾则循环回到数组开始位置。
            idx = (idx + 1) % objects.length;
        }
    }
}

使用合并散列解决Hash冲突

合并散列方法是简单高效结局Hash冲突的方法,它不需要额外的结构如链表或树。它适用于键值对数量较少、冲突较少的情况。在实际应用中,这种方法可以快速定位数据,并在冲突发生时有效地解决问题。然而,在有大量数据和频繁冲突的情况下性能将会下降

举例
想象有一家图书馆的书架管理。假设每个书架是一个哈希表中的槽位,每个书架上有一个特定的区域用于放置特定类别的书籍。这类似于通过哈希函数对书籍进行分类。

  1. 发生冲突:当一本新书到来,按照分类应该放到已经满了的书架上时,这相当于发生了哈希冲突。

  2. 合并散列:为了解决这个问题,图书馆管理员开始寻找最近的空闲书架(或书架上的空闲区域)。管理员将这本新书放在这个空闲位置,并在原本应该放置书籍的那个满了的书架上留下一张便签,标注新书的实际位置。

  3. 查找书籍:当读者查找这本书时,他们会先来到书籍原本应该放置的书架,发现便签后,便会根据便签上的指示去找到实际放置书籍的书架。

请添加图片描述

以下是代码示例

public class MyHashMap04ByCoalescedHashing<K, V> implements MyMap<K, V> {

    // 使用 Node 类型的数组来存储键值对。
    private Node<K, V>[] objects = new Node[8];

    /**
     * 向哈希映射中插入一个键值对。
     * 如果计算出的索引位置已有元素(即发生哈希冲突),则使用合并散列法处理冲突。
     *
     * @param key   要插入的键
     * @param value 对应的值
     */
    @Override
    public void put(K key, V value) {
        // 计算键的哈希码,并根据数组大小确定索引。
        int idx = key.hashCode() & (objects.length - 1);

        // 如果该索引位置为空,直接插入新节点。
        if (objects[idx] == null) {
            objects[idx] = new Node(key, value);
            return;
        }

        // 如果该索引位置的键与要添加的键相同,则替换节点。
        if (objects[idx].getKey().equals(key)) {
            objects[idx] = new Node(key, value);
            return;
        }

        // 发生哈希冲突,从数组末尾向前查找空闲位置。
        int cursor = objects.length - 1;
        while (objects[cursor] != null && !objects[cursor].getKey().equals(key)) {
            --cursor;
        }

        // 在找到的空闲位置创建新节点。
        objects[cursor] = new Node<>(key, value);

        // 更新原有碰撞位置节点,使其指向新节点。
        while (objects[idx].getIdxOfNext() != 0) {
            idx = objects[idx].getIdxOfNext();
        }
        objects[idx].setIdxOfNext(cursor);
    }

    /**
     * 根据键从哈希映射中获取对应的值。
     * 如果键不存在,则返回 null。
     *
     * @param key 要查找的键
     * @return 对应的值,如果键不存在则为 null
     */
    @Override
    public V get(K key) {
        // 同样使用键的哈希码计算索引。
        int idx = key.hashCode() & (objects.length - 1);
        // 遍历链表直到找到匹配的键或链表结束。
        while (objects[idx] != null && !objects[idx].getKey().equals(key)) {
            idx = objects[idx].getIdxOfNext();
        }

        // 返回找到的值或 null。
        return objects[idx] == null ? null : objects[idx].getValue();
    }
}

测试

public class Client {

    public static void main(String[] args) {
        System.out.println("============= 没有解决碰撞 ===============");
        MyMap<String, String> map = new MyHashMap<>();
        map.put("1","大黄");
        map.put("2","大白");
        System.out.println("碰撞前 key:"+"1"+" value:" + map.get("1"));

        // 下标碰撞
        map.put("9","猪宝");
        map.put("12","猪猪");
        System.out.println("碰撞后 key:"+"1"+" value:" + map.get("1"));
        System.out.println("碰撞后 key:"+"12"+" value:" + map.get("12"));
        System.out.println("============= 使用开放寻址解决碰撞 ===============");
        MyHashMap03ByOpenAddress<String, String> MyHashMapOpenAddress = new MyHashMap03ByOpenAddress<>();
        MyHashMapOpenAddress.put("1","大黄");
        MyHashMapOpenAddress.put("2","大白");
        System.out.println("碰撞前 key:"+"1"+" value:" + MyHashMapOpenAddress.get("1"));

        // 下标碰撞
        MyHashMapOpenAddress.put("9","猪宝");
        MyHashMapOpenAddress.put("12","猪猪");
        System.out.println("碰撞后 key:"+"1"+" value:" + MyHashMapOpenAddress.get("1"));
        System.out.println("碰撞后 key:"+"12"+" value:" + MyHashMapOpenAddress.get("12"));
        System.out.println("============= 使用拉链寻址解决碰撞 ===============");
        MyHashMap02BySeparateChaining<String, String> myHashMapSeparateChaining = new MyHashMap02BySeparateChaining<>();
        myHashMapSeparateChaining.put("1","大黄");
        myHashMapSeparateChaining.put("2","大白");
        System.out.println("碰撞前 key:"+"1"+" value:" + myHashMapSeparateChaining.get("1"));

        // 下标碰撞
        myHashMapSeparateChaining.put("9","猪宝");
        myHashMapSeparateChaining.put("12","猪猪");
        System.out.println("碰撞后 key:"+"1"+" value:" + myHashMapSeparateChaining.get("1"));
        System.out.println("碰撞后 key:"+"12"+" value:" + myHashMapSeparateChaining.get("12"));
        System.out.println("============= 使用合并散列解决碰撞 ===============");
        MyHashMap04ByCoalescedHashing<String, String> myHashMap04ByCoalescedHashing = new MyHashMap04ByCoalescedHashing<>();
        myHashMap04ByCoalescedHashing.put("1","大黄");
        myHashMap04ByCoalescedHashing.put("2","大白");
        System.out.println("碰撞前 key:"+"1"+" value:" + myHashMap04ByCoalescedHashing.get("1"));

        // 下标碰撞
        myHashMap04ByCoalescedHashing.put("9","猪宝");
        myHashMap04ByCoalescedHashing.put("12","猪猪");
        System.out.println("碰撞后 key:"+"12"+" value:" + myHashMap04ByCoalescedHashing.get("12"));
        System.out.println("碰撞后 key:"+"1"+" value:" + myHashMap04ByCoalescedHashing.get("1"));



    }
}
  • 65
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值