哈希表(HashMap、HashSet)

一、 什么是哈希表

  1. 是个存储结构:可以让我们一次从表中直接拿到想要的元素,时间复杂度为O(1)
  2. 为什么能实现O(1):通过哈希(散列)方法,使元素的存储位置和它的关键码之间建立一一映射的关系
    • 如果想要存取元素,都是利用哈希(散列)方法 + 关键码,从而计算出index位置,然后进行操作(怎么放的就怎么给它取出来
    • 哈希函数示例:hash(key) = key % capacity

二、 哈希冲突

2.1 为什么会出现冲突

  1. 原因:两个不一样的关键字通过相同的哈希函数映射到了相同的位置
    • 两个不一样的假如哈希函数是【hash(key) = key % capacity】,如果有两个key,分别为4和14,capacity为10,此时4和14生成的位置都是一样的

2.2 如何避免出现冲突

  1. 哈希冲突无法规避:由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,所以冲突的发生是必然的,我们能做的只是尽量降低冲突率
  2. 方式
    • 方式一:将哈希函数设置地更为合理。不过一般Java库已经帮我们写好了哈希方法,不需要程序员去设计
      • 哈希函数设计原则:【如果有m个元素,哈希出来的地址一定在0 ~ m-1】 + 【元素能够均匀地分布在整个空间里】 + 【简单】
      • 直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
      • 除留余数法
        • Hash(key) = key% p(p<=capacity),p是个最接近或者等于capacity的p
      • 平方取中法
      • 折叠法
      • 随机数法
      • 数学分析法
    • 方式二:调节负载因子
      在这里插入图片描述

2.3 出现冲突如何解决

  1. 解决方法一:闭散列(将key存到哈希冲突位置的其他空位置去)
    • 寻找空位置方法
      • 线性探测:找到下一个空的位置,然后把冲突的key放进去。但这样会把冲突的元素都挤在一起
      • 二次探测
        在这里插入图片描述
    • 闭散列缺陷
      • 数组利用率/空间利用率不高:利用率高的情况是把同样下标的放在一起,不占用其他格子
      • 不方便删除:假如4和14都在同一个下标,14放在了其他位置,但我们定义出来是在4下标,此时不好删除
  2. 解决方法二:开散列/哈希桶
    • 关于O(1)时间的复杂度
      • 虽然哈希表一直在强调哈希冲突,但其实实际中我们认为哈希表的冲突率是不高的,即每个桶中的链表长度是一个常熟。所以我们通常认为哈希表的插入/删除/查找的时间复杂度为O(1)

在这里插入图片描述

三、模拟实现哈希桶/开散列(整型数据)

3.1 结构

public class HashBucket {
	//Node相当于Entry
    static class Node {
        private int key;
        private int value;
        private Node next;

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

    public Node[] array;
    public int usedSize;
    
    //默认的负载因子为0.75
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    public HashBucket() {
        this.array = new Node[10];
    }
}

3.2 插入元素

  1. 思路
    • 首先根据key和哈希函数计算出对应的index,由于该index下包含了冲突的元素,所以我们需要遍历该链表
      • 重复的值需要更新:注意,因为HashMap是继承了Map接口,而Map的一大特点就是【如果有相同的key,会更新Value值】,所以如果有相同的我们需要更新
    • 如果遍历完发现没有重复的,就进行插入,可以头插也可以尾插,此处我们用的是尾插
    • 插入完毕后,需要计算负载因子,如果负载因子大于定义的值,就需要扩容
      • 扩容需要注意的问题:需要把桶中的数据一个个拿出来重新哈希到新的数组中。因为扩容后,原本的key哈希后得到的index很可能不是原来的index了,所以需要重新哈希。
public void put(int key,int val) {
    Node node = new Node(key,val);
    int index = key % array.length;
    //遍历index位置下方的链表
    Node cur = array[index];
    while (cur != null) {
        if(cur.key == key) {
            cur.value = val;
            return;
        }
        cur = cur.next;
    }
    //头插
    node.next = array[index];
    array[index] = node;

    usedSize++;
    //计算负载因子
    if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
        //扩容
        resize();
    }
}

//重新哈希原来的数据 !!!
private void resize() {
    //2倍扩容
    Node[] tmpArray = new Node[array.length * 2];
    //遍历原来的数组下标的每个链表
    for (int i = 0; i < array.length; i++) {
        Node cur = array[i];
        while (cur != null) {
            Node curNext = cur.next;//需要记录下来 原来链表的下一个节点的位置
            int index = cur.key % tmpArray.length;//新数组的位置
            //采用头插法 放到新数组的index位置
            cur.next = tmpArray[index];//这里修改之后 cur的next已经变了
            tmpArray[index] = cur;
            cur = curNext;
        }
    }
    array = tmpArray;
}

//计算负载因子
private float loadFactor() {
    return usedSize*1.0f / array.length;
}

3.3 获取元素

在这里插入图片描述

public int get(int key) {
    int index = key % array.length;
    Node cur = array[index];
    //变成红黑树的过程太复杂,此处不模拟
    while (cur != null) {
        if(cur.key == key) {
            return cur.value;
        }
        cur = cur.next;
    }
    return -1;
}

四、模拟实现哈希桶/开散列(泛型)

4.1 结构

public class HashBucket<K,V> {
    static class Node<K,V> {
        private K key;
        private V value;
        private Node<K,V> next;

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

    public Node<K,V>[] array;
    public int usedSize;
    
    //默认的负载因子为0.75
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    public HashBucket() {
        this.array = (Node<K,V>[])new Node[10];
    }
}

4.2 插入元素

public void put(K key,V val) {
    Node<K,V> node = new Node<>(key,val);
    int hash = key.hashCode();
    //控制成合理的位置,因为hashCode生成的数字一般都挺大,所以需要%
    int index = hash % array.length;
    Node<K,V> cur = array[index];
    while (cur != null) {
        if(cur.key.equals(key)) {
            cur.val = val; //如果val一样就更新
            return;
        }
        cur = cur.next;
    }

    node.next = array[index];
    array[index] = node;

    usedSize++;
    //计算负载因子
}

4.3 获取元素

  1. 代码解析
    • 自定义类型需要重写 equals 和 hashCode方法,hashCode用来找index位置,equals用来判断元素是否相同
class Person {
    public String id;

    public Person(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(id, person.id);
    }

    public int hashCode() {
        return Objects.hash(id);
    }
}
public V get(K key) {
    int hash = key.hashCode();
    int index = hash % array.length;//控制成合理的位置
    Node<K,V> cur = array[index];
    while (cur != null) {
        if(cur.key.equals(key)) {
            return cur.val;
        }
        cur = cur.next;
    }
    return null;
}
  1. 测试:因为此时person1和person2的hashCode结果是一样的,所以最后能打印出的name是【zhangsan】,即可以用person2去找到person1
public static void main(String[] args) {

    Person person1 = new Person("1234");
    Person person2 = new Person("1234");

    HashBuck<Person,String> hashBucket = new HashBucket<>();
    hashBuck.put(person1,"zhangsan");

    String name = hashBuck.get(person1);
    System.out.println(name); 

}

五、区别

5.1 TreeMap 和 HashMap 的区别

Map底层结构TreeMapHashMap
底层结构红黑树哈希桶
插入/删除/查找时间复杂度O(logN)O(1)
是否有序关于Key有序无序
线程安全不安全不安全
插入/删除/查找区别需要进行元素比较通过哈希函数计算哈希地址
比较与覆写key必须能够比较,否则会抛出 ClassCastException异常自定义类型需要覆写equals和 hashCode方法
应用场景需要Key有序场景下Key是否有序不关心,需要更高的时间性能

5.2 TreeSet 和 HashSet 的区别

Map底层结构TreeMapHashMap
底层结构红黑树哈希桶
插入/删除/查找时间复杂度O(logN)O(1)
是否有序关于Key有序无序
线程安全不安全不安全
插入/删除/查找区别按照红黑树的特性来进行插入和删除 先计算key哈希地址,然后进行插入和删除
比较与覆写key必须能够比较,否则会抛出 ClassCastException异常自定义类型需要覆写equals和 hashCode方法
应用场景需要Key有序场景下Key是否有序不关心,需要更高的时间性能

六、HashMap 源码分析

6.1 成员变量+结点定义

在这里插入图片描述

6.2 构造方法

  1. Map<String,Intger> map1 = new HashMap<>(1000):此时写着容量是1000,但实际上是2次幂数,容量为1024
    在这里插入图片描述

6.3 put()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值