极简极简的HashMap实现

符号表

符号表概述

我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息。
符号表有时也被称为字典。键就是单词,值就是单词对应的定义,发音和词源。
符号表有时又叫做索引。键就是术语,值就是书中该术语出现的所有页码。

  • 无序符号表HashMap
  • 有序符号表TreeMap

HashMap

概述

如果所有的键都是小整数,我们可以用一个数组来实现的符号表,将键作为数组的索引,而数组中对应的位置存储键关联的值。

哈希表是这种简单方法的扩展,并且能够处理更加复杂类型的键。我们需要用哈希函数将键转换成数组的索引

哈希表的核心算法可以分为两步。

  1. 用哈希函数将键转换为数组中的一个索引。理想情况下不同的键都能转换成不同的索引值。当然这只是理想情况下,所以我们需要处理两个或者多个键都散列到相同索引值的情况 (哈希碰撞)。
  2. 处理碰撞冲突。
    a. 开放地址法
    线性探测法, 平方探测法, 再散列法…
    b. 拉链法

哈希函数

一个优秀的 hash 算法,满足以下特点:

正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。

逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
只要逆向通过哈希得到一个明文,这个明文的哈希是我们给定的哈希,就是逆向。

输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。

冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。

我们可以简单地把哈希函数理解为在模拟随机映射,然后从随机映射的角度去理解哈希函数。

在数据结构中,对速度比较重视,对抗碰撞性不太看重。所以对哈希函数的要求没那么高,
只要满足下面两点就可以了。
计算速度快。
Hash值平均分布

处理碰撞冲突-拉链法

拉链法:如果我们想在常数时间复杂度内, 完成哈希表的增删查操作,那么我们就得控制链表的平均长度不超过某个值。
这个值我们称之为加载因子,也就是链表平均长度可以达到的最大值。因此,当元素个数达到一定的数目(threshold = size * table.length)的时候,我们就需要对数组进行扩容。

实现API

极简处理:
键不能为null。如果键为null,我们会抛出 NullPointerException.

值不能为null。我们这么规定的原因是:当键不存在的时候,get()方法会返回null。这样做有个好处,我们可以调用get()方法,看其返回值是否为 null 来判断键是否存在哈希表中。

缓存hash值。如果散列值计算很耗时 (比如长字符串)。那么我么可以在结点中用一个 hash 变量来保存它计算的 hash 值。Java 中的 String 就是这样做的。

当数组达到最大值时,并且链表的平均长度达到了最大值。这种情况,我们就破坏加载因子的限制,直接添加元素。

属性,构造方法,内部类Entry

这段代码我注意到的地方:

  • 在这里,将Entry设定成static内部类的原因:

    1:Entry不依赖于外部类存在。
    2:Entry[] table 数组不能是泛型数组,如果Entry是普通成员内部类,Entry依赖于外部类对象存在。外部类对象可能是带泛型的,编译器会报错。

  • 为什么要让table的长度是2的整数次幂。

因为key散列的数组的索引,是通过 hash(key) % table.length 求来的,% 是个很麻烦的计算,如果table.length = 2 ^ n, 这个% 就可以简化成 hash & (table.length - 1);

  • 如何保证把外部传入的initialCapacity转换成大于等于 initialCapacity的最小的整数次幂

就是下面这串代码***calculateCapacity(int initialCapacity)***,建议大家自己算一下,比如传入个7,二进制是 0000 0111
initialCapacity: 0000 0111
capacity : 0000 0110
capacity |= capacity >> 1 :
0000 0110
0000 0011
等于 0000 0111
capacity + 1 = 8

public class MyHashMapV2<K, V> {
    //属性
    private static final int DEFAULT_ARRAY_SIZE = 16;
    private static final int MAX_ARRAY_SIZE = 2^30;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    private Entry[] table;
    private int size;
    private int threshold;
    private float loadFactor;

    //内部类
    /*
    在这里,将Entry设定成static内部类的原因:
    1:Entry不依赖于外部类存在。
    2:Entry[] table 数组不能是泛型数组,如果Entry是普通成员内部类,Entry依赖于外部类对象存在。外部类对象可能是带泛型的,编译器会报错。
	*/
    private static class Entry {
        Object key;
        Object value;
        Entry next;
        int hash;

        public Entry(Object key, Object value, Entry next, int hash) {
            this.key = key;
            this.value = value;
            this.next = next;
            this.hash = hash;
        }
        @Override
        public String toString() {
            return  key +
                    " = " + value;
        }
    }
    //构造方法

    public MyHashMapV2() {
        this(DEFAULT_ARRAY_SIZE, DEFAULT_LOAD_FACTOR);
    }
    public MyHashMapV2(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public MyHashMapV2(int initialCapacity, float loadFactor){
        if(initialCapacity <= 0 || initialCapacity > MAX_ARRAY_SIZE) throw new IllegalArgumentException("initialCapacity = " + initialCapacity);
        if(loadFactor <= 0) throw new IllegalArgumentException("loadFactor = " + loadFactor);
        this.loadFactor =loadFactor;
        int capacity = calculateCapacity(initialCapacity);
        /*table = new MyHashMapV2.Entry[capacity];  Entry是普通成员内部类时,应该这样用*/
        table = new Entry[capacity];
        threshold = (int)(table.length * loadFactor);
    }
/*1: 为什么要让table的长度是2的整数次幂。因为key散列的数组的索引,是通过 hash(key) % table.length 求来的,% 是个很麻烦的计算,如果table.length = 2 ^ n, 这个% 就可以简化成 hash & (table.length - 1);
2: 如何保证把外部传入的initialCapacity转换成大于等于initialCapacity的最小的整数次幂,就是下面这串代码,建议大家自己算一下,比如传入个7,二进制是 0000 0111 
 initialCapacity: 0000 0111 
 capacity 		: 0000 0110
 capacity |= capacity >> 1 :
     			  0000 0110
     			  0000 0011
     		等于  0000 0111  
 capacity + 1 = 8
*/
    private int calculateCapacity(int initialCapacity) {
        int capacity = initialCapacity - 1;
        capacity |= capacity >> 1;//保证前两位是1
        capacity |= capacity >> 2;//保证前四位是1
        capacity |= capacity >> 4;//前八位
        capacity |= capacity >> 8;//16
        capacity |= capacity >> 16;//32
        return capacity + 1;
    }

V put(K key, V value)

我注意到的地方:

  • 判断是否需要扩容,采用size >= threshold方法,考量如下:
    * 1:假设说table.length 是16, factor 是0.75,threshold是 12,那就是如果size >= 12(整个哈希表存了12个键值对及以上),就要扩一次.
    * 因为考虑到为了让时间和空间平衡链表并不一定平均分布,loadFactor表示平均链表长度,当size == threshold时,可能有些链已经很长了
    * 2:为什么是 size>= threshold, 而不是 size == threshold, 因为如果是多线程并发,可能会出现size > threshold的情况。
public V put(K key, V value){
        if(key == null || value == null){throw new IllegalArgumentException("key or value cannot be null");}
        int hash = hash(key);
        int index = indexFor(hash, table.length);
        //遍历链表,判断key是否存在在链表中,如果存在,更新键值对,并返回历史value
        for(Entry node = table[index]; node != null; node = node.next){
            if(node.hash == hash && (node.key == key || key.equals(node.key))) {
                V oldValue = (V)node.value;
                node.value = value;
                return oldValue;
            }
        }
        //key没在链表中,或者链表table[inded] == null
        addEntry(key, value, hash, index);
        return null;
    }

    private void addEntry(K key, V value, int hash, int index) {
        /*判断是否需要扩容,采用size >= threshold方法,考量如下:
        * 1:假设说table.length 是16, factor 是0.75,threshold是 12,那就是如果size >= 12(整个哈希表存了12个键值对及以上),就要扩一次.
        *    因为考虑到为了让时间和空间平衡,链表并不一定平均分布,loadFactor表示平均链表长度,当size == threshold时,可能有些链已经很长了
        * 2:为什么是 size>= threshold, 而不是 size == threshold, 因为如果是多线程并发,可能会出现size > threshold的情况。
        *  */
        if(size >= threshold){
            //如果数组已经是最大容量,那就破坏加载因子
            if(table.length == MAX_ARRAY_SIZE){
                threshold = Integer.MAX_VALUE;
            }
            else {
                int newCapacity = table.length << 1;
                grow(newCapacity);
                index = indexFor(hash, table.length); //table扩容以后,key的hash值没有变,但是因为table长度变了,所以index会发生变化,
            }
        }
        //链表内插入键值对,采用头插法
        Entry node = new Entry(key, value, table[index], hash);
        table[index] = node;
        size++;
    }

    private void grow(int newCapacity) {
        /*不需要判断,因为在addEntry判断是否需要扩容得到时候,已经判断了。
        if(newCapacity > MAX_ARRAY_SIZE){
            newCapacity = MAX_ARRAY_SIZE;
        }*/
        Entry[] newTable = new Entry[newCapacity];
        //因为链表中的Entry可能hash值不同,在table没有扩容前,可能在当前索引下,但是扩容以后,也许索引会发生变化,所以要对每一个键值对重新散列。
        for(int i = 0; i < table.length; i++){
            Entry node = table[i];
            while(node != null){
                int index = indexFor(node.hash, newCapacity);
                Entry next = node.next;
                newTable[i] = node;
                node.next = newTable[i];
                node = next;
            }
        }
        /*需要重新散列,所以不能直接把数组这么赋值过去
        for(int i = 0; i < table.length; i++){
            newTable[i] = table[i];
        }*/
        table = newTable;
        threshold = (int)(table.length * loadFactor);
    }

    private int indexFor(int hash, int length) {
        return hash & (length - 1);
    }

    private int hash(K key){
        int hash = key.hashCode();
        return (hash << 16) ^ (hash >> 16);
    }

V delete(K key)

public V delete(K key){
        if(key == null){throw new IllegalArgumentException("key cannot be null");}
        int hash = hash(key);
        int index = indexFor(hash, table.length);
        Entry parent = null;
        for(Entry node = table[index]; node != null; node = node.next){
            if(hash == node.hash && (key == node.key || key.equals(node.key))){

                if(parent == null) {
                    table[index] = table[index].next;
                    size--;
                    return (V)node.value;
                }
                else {
                    parent.next = node.next;
                    size--;
                    return (V)node.value;
                }
            }
            parent = node;
        }
        return null;
    }

V get(K key)

 public V get(K key){
        if(key == null){throw new IllegalArgumentException("key cannot be null");}
        int hash = hash(key);
        int index = indexFor(hash, table.length);
        for(Entry node = table[index]; node != null; node = node.next){
            if(hash == node.hash && (key == node.key || key.equals(node.key))){
                return (V)node.value;
            }
        }
        return null;
    }

Set keys()

Set<K> keys() {
        Set<K> set = new LinkedHashSet<>();
        for(Entry node: table){
            while(node != null){
                set.add((K)node.key);
                node = node.next;
            }
        }
        return set;
    }

其他方法(clear, contains, isEmpty, size)

public void clear(){
        for(int i = 0; i < table.length; i++){
            table[i] = null;
        }
        size = 0;
    }

    public boolean contains(K key){
        return (get(key) != null);
    }

    public boolean isEmpty(){
        return size == 0;
    }

    int size() {
        return size;
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值