哈希表以及HashMap代码实现

哈希表

前面博客已经介绍过HashMap和HashSet,不太了解的朋友可以看看: HashMap和TreeMap

在聊哈希表之前我们先聊聊搜索

一、关于搜索相关的算法和数据结构

1、为什么搜索很重要

计算机中针对数据结构主要抽象出的是四个操作:增/删/改/查

其中查找的使用频率最高

2、搜索的通用模型是什么

在一组关键字集合中,找到指定关键字的过程

  1. 只需要找关键字------判断是否存在------纯Key模型

    纯Key模型 HashSet TreeSet

  2. 同时找关键字相关联的数据-----Key-Value模型

    Key-Value模型 HashMap TreeMap

3、场景分类
  1. 关键字集合基本不变。

    最适合算法:二分查找(前提:数据有序)

  2. 关键字变化比较频繁

    两类数据结构实现 哈希表 vs 平衡搜索树

二、哈希表

1、哈希表查找

前提:利用数组 下标访问元素时间复杂度为O(1)

哈希表的整体思路:把一个很大的keySet 的查找过程,转换为很多个小 keyset 的查找过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CKnBR7uh-1585030225316)(C:\Users\惠秋丽\AppData\Roaming\Typora\typora-user-images\1584978131587.png)]

那么问题来了怎么通过key来的得到需要放的下标呢?

这里面我们需要先注意一个问题,key可以为任意的类型,但是我们想要得到的是下标是 int 类型,这个时候就需要用到哈希函数来帮助我们,相同的key通过哈希函数的计算会得到相同的 int 类型的数字(哈希值)。

这个时候又有可能出现问题了,因为keyset 中的 key 数量 是远远大于数组的长度,这样的话,不同的key,经过哈希函数的计算就会得到相同的hash值 (名为冲突)(就是上图中的小集合),这个时候应该怎么办?

  • 你肯定想到了数组扩容,这个是不现实的,因为太浪费空间了

  • 那应该怎么办?答案是冲突是不可避免的,因为 把 M 个 数 ,放到 N 个下标中(M 远大于 N),所以肯定会冲突

  • 那肿么办?我们能做的只是减少冲突的发生,使冲突呈现一个比较好的形态,那么问题又来了怎么减少冲突呢?

  • 冲突就是我们上面所说的小集合,我们想要减少冲突,那么我们就需要设计一个比较好的哈希函数,使得下标尽可能的均匀

  • 这个时候我们需要引进来几个概念

    • 冲突率 = 插入一个新的 key ,会遇到冲突的概率

    • 负载因子 = 所有 key 的数量 / 数组的长度

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fa7p5YLY-1585030225318)(C:\Users\惠秋丽\AppData\Roaming\Typora\typora-user-images\1584979856064.png)]

  • 知道了前面这几个概念,我们就可以来想想怎么减少冲突?我们可以通过把负载因子控制在一个阈值范围内来达到目的。即通过减少负载因子来减少冲突率。具体的做法是,当负载因子超过某个阈值的时候,我们就增加数组的长度(在Java中,默认的负载因子为 0.75 ,又名扩容因子)

  • 扩容后数组的长度必须为 2!(数组的长度一直都应该2!)(原因后面讲)

  • 但是这个只是解决“怎么减少冲突”,但是根据阈值来看,冲突还是会发生的,那么发生了冲突应该怎么办?这里提供两个方案,其实方案很多。

    1. 数组内部解决(闭散列)-----有个印象即可
    2. 另起炉灶,把所有的冲突 key 放到另外的集合
      1. 可以使用链表 ---- 我们认为冲突的 key 不会太多(HashMap实现JDK1.7:数组+链表)
      2. 红黑树----冲突多了(HashMap实现JDK1.8:数组+链表+红黑树)

2、聊聊put的过程

  1. 通过 key 得到一个下标(index)

    1. 如果key为自定义类型,需要通过key得到hash值,必须重写hashCode()。当两个对象相同时,得到的哈希值也应该相同,则需要重写equals()方法

    2. 哈希值比一定是在[0,array.length)里面,即不一定是合法的下标

      1. 这时候我们就会想到可以采用 hash % array.length ,这样就可以得到合法的下标,但是Java没有这样子,因为这样操作相对比较慢

      2. Java采取另一中方式,这种方式需要满足一个前提 array.length 一定是 2 的 n 次方

        int index = (array.length - 1) & hash

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YfN2O0dz-1585030225318)(C:\Users\惠秋丽\AppData\Roaming\Typora\typora-user-images\1584981433514.png)]

        上面 这种方式,导致 hash 中真正被用到的只有后面 4 bit,没有把全部的bit 全部用上,所以会导致下标不均匀。

        Java这个时候就多做了一件事

      3. hash = (hash >>> 16) ^ hash

        index = (array.length - 1) & hash

        利用这种方式使得小标均匀

  2. 因为Java内部是用拉链法解决冲突的,用下标,只能找到对应的小集合即可(链表)

    利用数组下标访问是O(1)的特性

  3. 在小集合中查找对应的 key 所在的节点

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ESDEtRFl-1585030225319)(C:\Users\惠秋丽\AppData\Roaming\Typora\typora-user-images\1584981866073.png)]

3、聊聊HashMap的树化过程

为什么需要树化?

这个本质上,是一个安全问题。因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶中,则会形成一个链表。而链表查询时线性的,会严重影响存取的性能。

在现实生活中,可能在某个index位置处,key过多的可能性

原因:key 的分布不符合理想的分布。(理想情况下, key 的数量巨大时,都是符合高斯分布(正态分布))

​ 比如,黑客知道了哈希函数,就会构造一组 key ,必然发生冲突。这就导致了,在某一下标处,链表的长 度特别的长,这就违背了哈希表的最大思想----把大数据集的查找转换为小数据集的查找.

​ 怎么解决呢?再次使用查找用的数据结构(哈希表,搜索树)上去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VskvKptu-1585030225319)(C:\Users\惠秋丽\AppData\Roaming\Typora\typora-user-images\1584982583556.png)]

说明:

  1. 树的情况时非常少的
  2. 树化是需要阈值的,选择了8 ,因为泊松分布。

4、自己实现HashMap

Map接口:

package map;

public interface Map<K,V> {
    V get(K key);
    V put(K key ,V value);
}

HashMap类:

package map;

public class HashMap<K,V> implements Map<K,V> {

    private static class Entry<K ,V> {
        K key;
        V value;
        Entry<K,V> next;

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

    // 16 就是2 的 n 次方
    private Entry<K,V>[] table = new Entry[16];
    private int size;
    private static final double LOAD_FACTOR_THRESHOLD = 0.75;

    @Override
    public V get(K key) {
        int hash = key.hashCode();
        hash = (hash >>> 16) ^ hash;
        int index = (table.length - 1) & hash;
        // 得到的是头结点
        Entry<K,V> head = table[index];
        // 在链表中查找
        Entry<K,V> node  = head;
        while (node != null) {
            if(key.equals(node.key)) {
                return node.value;
            }
            node = node.next;
        }
        return null;
    }

    @Override
    public V put(K key, V value) {
        int hash = key.hashCode();
        hash  = (hash >>> 16) ^ hash;
        int index = (table.length - 1) & hash;
        Entry<K,V> head = table[index];
        Entry<K,V> node = head;
        // 在链表中查
        while (node != null) {
            if(key.equals(node.key)) {
                V oldValue = node.value;
                node.value = value;
                return oldValue;
            }
            node = node.next;
        }
        // 没有找到,所以插入节点
        Entry<K,V> newNode = new Entry<>(key,value);
        // 头插和尾插都可以
        // 尾插
        if(head == null) {
            table[index] = newNode;
        }else {
            Entry<K,V> last = head;
            while (last.next != null) {
                last = last.next;
            }
            last.next = newNode;
        }
        size ++;

        // 通过调整负载因子,来控制冲突
        if((double) size / table.length >= LOAD_FACTOR_THRESHOLD) {
            // 扩容
            resize();
        }
        return null;
    }

    /*
     * 需要把所有的 key 重新计算 hash,重新插入
     */
    private void resize() {
        // 保证新的长度也是 2 的 n 次方
        Entry<K,V>[] newTable = new Entry[table.length * 2];

        // 遍历所有的 key
        // 首先遍历所有的下标位置,找到一条条的链表
        // 在次遍历每个链表,找到一个个的 key
        for (int i = 0; i < table.length ; i++) {
            Entry<K,V> node = table[i];
            while (node != null) {
                // 为了简便,重新创建节点
                Entry<K,V> newNode = new Entry<>(node.key,node.value);
                int hash = node.hashCode();
                hash = (hash >>> 16) ^ hash;
                int index = hash & (newTable.length - 1);   // 这里是新数组
                // 使用头插,简单一点
                newNode.next = newTable[index];
                newTable[index] = newNode;

                node = node.next;
            }
        }
    }
}

总结:

  • put的过程
    1. 计算出 key 的 hash值
    2. 找出 key 所在数组的下标index
    3. 在链表中查找,如果有这个 key 已经存在了,那么就替换原来的 value 值
    4. 如果不存在,那么就插入节点
    5. 判断 负载因子 和 0.75 的大小关系,如果负载因子 >= 0.75,需要扩容
    6. 创建新的数组newTable,长度为原来数组的2倍
    7. 遍历数组的所有下标位置,再遍历所有下标位置的链表,重新计算 hash值,重新做插入
  • get的过程
    1. 计算出 key 的 hash值
    2. 找出 key 所在数组的下标index
    3. 找的数组下标index的链表,如果没有返回null,表示没找到
    4. 如果有,遍历这条链表,返回 key 所对应 的 value
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值