自我实现 Java 标准库中HashMap(简易版)

主要想和大家分享哈希表的相关内容

哈希表是一种拥有随机访问能力的表.

它是通过一个映射函数将元素的内容映射到对应的地址或者位置,从而减少了遍历过程中的消耗.

常见的哈希关系有取余的方法,这也是最简单的.

通过取余的方式我们可以轻易地得到元素对应的位置,那么问题来了,如果该位置已经有元素占用了怎么办(哈希冲突发生)?

常见的做法有两种:

1. 闭散列

如果发生冲突,就沿着哈希表继续往下找,找到下一个空的位置,按照这个思路,存放元素的时候好说,查找和删除可就麻烦了,而且要是冲突比较多,这些操作就相当于遍历数组,效率会很受影响.所以在实际中我们并不会使用这种方式,

2. 开散列

如果某个位置发生冲突,我们就让这个位置变成一个链表或者二叉树.

Java 标准库中的 HashMap 就是采用了这种方式,当某个位置发生冲突,就将这个位置先变成链表,如果冲突严重再将这个位置变成红黑树.

hash 函数到底怎么设计?

实际上我们不用自己设置 hash 函数,完全可以采用现成的

例如 md5 ,md5 主要用于给字符串计算 hash 值,我们前面说数字类型可以取余来完成,但是字符串就不可以这样了,而 md5 可以将字符串的内容变成一个数据,然后再采用取余的方式生成哈希函数即可.

它有如下的特点:

定长:无论输入的数据多长,得到的 md5 都是定长的.

分散:输入的数据稍有变化,得到的 md5 就会相差很大.

不可逆:根据 md5 的值几乎无法返回成原来的字符串.

正是由于这些特点, md5 也经常用来加密.

 

Java 标准库中的 HashMap 的底层实现就是 哈希表

下面是我根据哈希的思想实现的一个简单的 HashMap

class HashNode {
    public int key;
    public int value;
    public HashNode next;

    public HashNode(int key, int value) {
        this.key = key;
        this.value = value;
    }
}

public class MyHashMap {
    // 哈希表
    private HashNode[] array = new HashNode[16];
    // 表中的元素个数
    private int size = 0;

    // 哈希函数,简单求余
    private int hash(int key) {
        // 可以设计的更为复杂,比如根据 key 计算 md5 在求余
        return key % array.length;
    }

    // 根据 key 的值查找 value
    public Integer get(int key) {
        int index = hash(key);
        for (HashNode cur = array[index]; cur != null; cur = cur.next) {
            if (cur.key == key) {
                // 找到了
                return cur.value;
            }
        }
        return null;
    }

    // 根据 key 的值查找 value
    // 如果查找不到,返回默认值
    public Integer getOrDefault(int key, int defaultVal) {
        Integer ret = get(key);
        if (ret == null) {
            return defaultVal;
        }
        return ret;
    }

    // 插入新的键值对
    public void put(int key, int value) {
        int index = hash(key);
        if (array[index] == null) {
            // 如果这个位置还没有元素
            array[index] = new HashNode(key, value);
        }else {
            // 这个位置已经有元素了,即发生哈希冲突,
            // 我们先在这个位置的链表查找存不存在该 key 值
            for (HashNode cur = array[index]; cur != null; cur = cur.next) {
                if (cur.key == key) {
                    // 这个 key 已经存在了,现在修改这个 value 即可
                    cur.value = value;
                    return;
                }
            }
            // 循环结束也没有发现和 key 相等的,新建节点,头插在此
            HashNode newNode = new HashNode(key, value);
            newNode.next = array[index];
            array[index] = newNode;
        }
        size++;
        // 到这里插入已经成功了
        // 但是可能疯狂插入导致每个链表都很长,使得查找的效率低下,
        // 所以当哈希表拥挤到一定程度的时候我们要进行扩容
        // loadFactor() 是负载因子 = 元素个数 / 数组长度
        if (loadFactor() > 0.75) {
            // 扩容
            resize();
        }
        return;
    }

    private void resize() {
        // 使用更大的数组存放
        HashNode[] newArray = new HashNode[2 * array.length];
        // 搬运旧表上的元素
        for (int i = 0; i < array.length; i++) {
            HashNode next = null;
            for (HashNode cur = array[i]; cur != null; cur = next) {
                next = cur.next;
                int index = cur.key % newArray.length;
                // 插入新表
                cur.next = newArray[index];
                newArray[index] = cur;
            }
        }
        array = newArray;
    }

    private double loadFactor() {
        return (double) size / array.length;
    }

    // 根据 key 的值删除对应的键值对
    // 删除成功返回 key 对应的 value ,失败返回 null
    public Integer remove(int key) {
        int index = hash(key);
        Integer ret = null;
        if (array[index] == null) {
            // 这个链表直接为空,查找失败
            return null;
        }
        if (array[index].key == key) {
            // 说明待删除的节点是链表头结点
            ret = array[index].value;
            array[index] = array[index].next;
            size--;
            return ret;
        }
        // 待删除的节点可能在中间某个位置
        HashNode pre = null;
        for (HashNode cur = array[index]; cur != null; pre = cur, cur = cur.next) {
            if (cur.key == key) {
                // 找到了
                ret = cur.value;
                pre.next = cur.next;
                size--;
                return ret;
            }
        }
        // 循环结束都没找到,说明没有
        return null;
    }

}

核心的操作是插入操作 put 

在 put 方法中有两个需要注意的点:

1. 负载因子,负载因子用来衡量一个哈希表的拥挤程度(发生冲突的概率),其值 = 元素个数 / 数组长度,当这个值过大的时候哈希表会进行扩容操作.Java 标准库中这个临界值是 0.75 ,太低表的利用率很低,太高影响查找效率.

2. 扩容函数,当负载因子超过 0.75 时,哈希表会自动进行扩容,将数组的容量变为原来的2倍( Java 标准库中,是以移位的形式进行的).

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值