Java中HashMap的理解

一、hash表时如何提升查找效率

利用的前提:数组中根据下标访问元素的时间复杂度是O(1)

查找过程:

  • 在一个Key-Set中查找指定Key的过程。
  • 因为Key-Set中的key太多了,所以查找会变慢。
  • 如果Key-Set中的Key较少,无论什么查找算法都比较快。

Hash表的大体思路:把一个Key-Set的查找过程转换为很多个小的Key-Set的查找过程。

在10000个Key中查找key
在这里插入图片描述
put(key, value)的一次过程

  • 如何通过key得到存放的下标?

    • 因为key是一个任意的数据类型,要得到的下标是一个int类型,所以需要经过哈希函数,把key转换成一个int类型的数字
      • 要求:相同的key,得到的int类型数字一定是相同的
      • int类型的数字不一定是合法的下标,还需要将其转换为一个合法的下标
  • 通过下标,找到小集合(冲突问题

  • 在小集合中,选择合适的算法,找到对应的key以及关联的value

冲突(Collision)

  • 什么是冲突?
    不同的key,经过hash函数后,得到了相同的hash值

  • 为什么会产生冲突?
    因为Key-Set中的key的数量是远远大于数组长度的。

  • 冲突可以完全消除吗?
    不可以。把M个数,放到N个下标中(M远远大于N)就一定会产生冲突。

  • 对于冲突我们的原则:尽可能的减少冲突,如果真的遇到冲突了,也可以解决。

  • 如何尽可能的减少冲突?

    • 如果一定有冲突,冲突呈现一个较好的形态。
      冲突就是上面的小集合,小集合的key数量越平均越好。
      所以,需要设计比较好的哈希函数,使得到的下标尽可能的均匀。

    • 插入一个新的key会有概率冲突

    • 负载因子 = 所以key的数量 / 数组的长度
      冲突率会随着负载因子变大而变大。

    • 我们的目标是把冲突控制在一个可接受的范围里,我们可以通过把负载因子控制在一个阈值范围内来达到目的

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

    • key的数量是不能改变的,通过增加数组的长度来减小负载因子

      • 为了控制冲突率,会设置一个阈值,当负载因子超过这个阈值时,需要增加数组长度,也就是所谓的扩容
        Java中默认阈值是 0.75(LoadFactor)—— 扩容因子
      • 为什么Hash表中的扩容时机和顺序表是完全不同的?
        顺序表的扩容是为了解决放不下的问题 —— 只要放得下,就不需要扩容
        哈希表的扩容是为了降低冲突的问题 —— 放得下,也会扩容
  • 如何解决冲突?

    • 数组内部解决(闭散列
    • 另起炉灶,把所有的冲突key放到另外的结合
      • 可以使用链表——因为我们认为冲突的key不会太多
      • 冲突的数量变多了,链表变成平衡搜索树
二、和Java语言强相关

1.hashMap.put(key, value)的过程

  • 通过key得到一个下标index O(1)的时间复杂度
    • Hash Map的key其实是一个泛型,本质就是一个Object的子类
      • 通过key得到哈希值(hash)
        **hashCode()**属于Object,用来求出key所对应的哈希值
        int hash = key.hashCode();
      • hash 不保证是在[0, arr.length) ,不一定是合法下标
        再把hash转换为一个合法的下标
        1.通过 hash % array.length 得到合法下标
        Java中没有这种方式,因为mod操作相对比较慢
        2.Java选择了另一种方式,需要一个前提,array.length一定是2的n次方;初始长度为16
        int index = (array.length - 1) & hash
        array.length = 16 二进制表示 0b10000
        array.length - 1 = 15 二进制表示 0b01111
        hash & (array.length - 1)
        无论原来的hash是多大的一个数,只能保留4bit使得结果一定不超过16,最多到15,[0, 16)一定是一个合法下标。
        但是这样,导致hash中真正被用的只有后4bit,因为没有用到所有bit,所以可能导致下标不均匀。
        Java多做了一个事情:
        hash = (hash >>> 16) ^ hash;
        index = (array.length - 1) & hash;
        使得下标尽可能均匀。

自定义类(Person)作为HashMap的key,做到认为相同的Person对象,必须返回相同的哈希值。

import java.util.Objects;
public class Person {
    private String name;
    private int age;
    private int gender;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Person person = (Person) o;
        return age == person.age &&
                gender == person.gender &&
                name.equals(person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, gender);
    }


    public static void main(String[] args) {
        MyHashMap<Person, Integer> map = new MyHashMap<>();

        Person p1 = new Person();
        p1.name = "你好";
        p1.age = 18;
        p1.gender = 1;

        Person p2 = new Person();
        p2.name = "你好";
        p2.age = 18;
        p2.gender = 1;

        map.put(p1, 108);
        //因为覆写了hashCode 和 equals,代码才能正确运行
        //返回108,否则返回 null
        System.out.println(map.get(p2));
    }
}

2.自己实现HashMap

public interface MyMap<K, V> {
    V get(K key);

    V put(K key, V value);
}

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

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

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

    private MyEntry<K, V>[] table = new MyEntry[16];
    private int size = 0;
    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 = hash & (table.length - 1);

        MyEntry<K, V> head = table[index];
        MyEntry<K, V> cur = head;
        while (cur != null) {
            if (key.equals(cur.key)) {
                return cur.value;
            }
            cur = cur.next;
        }
        return null;
    }


    @Override
    public V put(K key, V value) {
        int hash = key.hashCode();
        hash = (hash >>> 16) ^ hash;
        int index = hash & (table.length - 1);

        MyEntry<K, V> head = table[index];

        //在链表中查找
        MyEntry<K, V> cur = head;
        while (cur != null) {
            if (key.equals(cur.key)) {
                V oldValue = cur.value;
                cur.value = value;
                return oldValue;
            }
            cur = cur.next;
        }


        //没有找到节点
        MyEntry<K, V> newNode = new MyEntry<>(key, value);
        /**
         * 头插
         * newNode.next = head;
         * table[index] = newNode;
         */

        //尾插
        if (head == null) {
            table[index] = newNode;
        } else {
            MyEntry<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;
    }

    private void resize() {
        MyEntry<K, V>[] newTable = new MyEntry[table.length * 2];

        // 遍历所有的 key
        // 首先遍历所有的下标位置,找到一条条的链表
        // 再次遍历每个链表,找到一个个的 key
        for (int i = 0; i < size; i++) {
            MyEntry<K, V> node = table[i];
            while (node != null) {
                // 为了简化,重新创建新结点
                MyEntry<K, V> newNode = new MyEntry<>(node.key, node.value);
                int hash = newNode.key.hashCode();
                hash = (hash >>> 16) ^ hash;
                int index = hash & (newTable.length - 1);

                //头插
                newNode.next = newTable[index];
                newTable[index] = newNode;

                node = node.next;
            }
        }
    }
}

2.因为Java内部是用拉链法解决冲突的,用下标,只找到对应的小集合即可(默认是链表)
利用了数组下标访问时间复杂度是O(1)的特性

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

Node node = array[index];
while (node != null) {
	if (key.equals(node.key)) {
		表示找到
	}
	node = node.next;
}
HashMap的树化过程(Treeify)

为什么要树化?
理想情况下,根据概率论中的泊松分布计算,每个下标处,key的个数不会太长
在这里插入图片描述
但实际中,还是有可能出现某个index位置处,key过多的情况。
原因:key的分布不是符合理想分布。(理想情况下,key的数量巨大时,都是符合高斯分布(正态分布)) key不是正态分布

如果某个下标处,链表的长度特别长,违背了哈希表的思想——把大数据集的查找转化为小数据集的查找。
所谓的小数据集也很大,所以哈希表的查找变慢了。

怎么解决?
Java,现在小数据集查找很慢,再次使用查找用的数据结果(搜索树)上去。
在这里插入图片描述
树化的情况是比较少的。
当链表长度超过阈值(8)时,将链表转换为红黑树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值