【Java】哈希表 HashMap&HasSet

目录

哈希表

哈希(散列)函数:

哈希冲突 :

减少冲突:

1.优化哈希函数

2.闭散列

3.开散列(哈希桶)

简单实现哈希桶(无树化)

HashMap&HashSet

HashMap注意事项:

重写equals和hashCode:

调用不带参数的构造方法,第一次put()时才开辟内存:

 初始化空间大小:

 什么时候树化:


哈希表

哈希(散列)函数:

通过这个函数,可以使得存放的与其存储位置产生一一对应的关系,那这个函数就时哈希(散列)函数。比如

哈希冲突 :

可以看到,存放数据的时候会出现有的数据改存的位置被存放了,这样的情况就叫哈希冲突。

负载因子:哈希表中的数据个数 / 表长 

负载因子越大,冲突概率越大。

 

减少冲突:

1.优化哈希函数

哈希函数的定义域必须包括需要存储的全部关键码,而如果哈希表允许有 m 个地址时,其值域必须在 0 m-1 之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单

 常见的哈希函数有

1. 直接定制法 --( 常用 )
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况 
2. 除留余数法 --( 常用 )
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:
Hash(key) = key% p(p<=m), 将关键码转换成哈希地址

2.闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 key 存放到冲突位置中的 下一个 空位置中去。如何放到空位呢?

     1. 线性探测  ,如上图,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

    2. 二次探测 ,和线下探测基本一样,只不过下一个空位置的方法为:newIndex = (oldIndex+ i² )% m, 或者:newIndex = (oldIndex - i²)% m。其中:i = 1,2,3…

       这两种方法都不是Java所使用的方法,Java使用的是开散列。

3.开散列(哈希桶)

开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

简单实现哈希桶(无树化)

import java.util.Arrays;
import java.util.HashMap;

// key-value 模型
public class HashBucket {
    private static class Node {
        private int key;
        private int value;
        private Node next;
 
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private Node[]  array;
    private int size;   // 当前的数据个数
    private static final double LOAD_FACTOR = 0.75;//负载因子
    private static final int DEFAULT_SIZE = 8;//默认桶的大小

    public HashBucket() {
        array = new Node[8];
        size = 0;
    }
 
    public void put(int key, int value) {
        Node node = new Node(key, value);
        int tmp = key % array.length;
        //不在数组里
        if (array[tmp] != null) {
            //使用尾插法放到哈希桶后面
            Node cur = array[tmp];
            while (cur.next != null) {
                cur = cur.next;
            }
            cur.next = node;
        } else {
            array[tmp] = node;
        }
        size++;
        double LoadFactor = loadFactor();
        if (LoadFactor >= LOAD_FACTOR) {
            //扩容,重放
            resize();
        }
    }

    //扩容
    private void resize() {
        int oldLength = array.length;
        Node[] newArray = new Node[oldLength * 2];
        int newLength = newArray.length;
        for (int i = 0; i < oldLength; i++) {
            //遍历原数组当中的数据
            Node outCur = array[i];
            //数据不为空进行复制
            if (outCur != null) {
                //数组内部用新引用指向
                Node inCur = outCur;
                //内部的链表可能是一串,所以用while循环
                while (inCur != null) {
                    int tmp = inCur.key % newLength;
                    //如果新数组有数据,则使用尾插法
                    if (newArray[tmp] != null) {
                        //cur找到尾
                        Node cur = newArray[tmp];
                        while (cur.next != null) {
                            cur = cur.next;
                        }
                        //此时cur指向了链表的最后一个结点
                        //创建一个新结点是为了不改变inCur,因为插入后,next必须置空,这样就找不到后面了
                        Node node = new Node(inCur.key, inCur.value);
                        cur.next = node;
                    }
                    //如果新数组没有数据,直接插入,创建结点理由同上
                    else {
                        Node node = new Node(inCur.key, inCur.value);
                        newArray[tmp] = node;
                    }
                    inCur = inCur.next;
                }
            }
        }
        array = newArray;
    }
    
//    头插法
//    private void resize() {
//        Node[] newArray = new Node[2*array.length];
//        for (int i = 0; i < array.length; i++) {
//            Node cur = array[i];
//            while (cur != null) {
//                Node curNext = cur.next;
//                int newIndex = cur.key % newArray.length;
//                cur.next = newArray[newIndex];
//                newArray[newIndex] = cur;
//                cur = curNext;
//            }
//        }
//        array = newArray;
//    }

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


    public int get(int key) {
         int tmp = key % array.length;
         if (array[tmp] == null) {
             return -1;
         } else {
             Node cur = array[tmp];
             while (cur.next != null) {
                 if (cur.key == key) {
                     return cur.value;
                 } else {
                     cur = cur.next;
                 }
             }
             return -1;
         }
    }
}

HashMap&HashSet

HashMap是实现了Map接口的一个类,它的底层是哈希桶。

HashMap底层结构哈希桶

插入 / 删除 / 查找时间

复杂度

O(1)
是否有序
线程安全不安全
应用场景
Key 是否有序不关心,需要更高的
时间性能

HashSet是实现了Set接口的一个类,底层是HashMap。

HashMap注意事项:

重写equals和hashCode:

对于自定义类的方法,相较于TreeMap实现Comparable或者Comparator,HashMap则需要重写equals和hashCode方法。

import java.util.HashMap;
import java.util.Map;

class Student {
    public String id;
    public String name;

    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" + id + " "+ name + "}";
    }

}

public class TestHash {

    public static void main(String[] args) {
        HashMap<Student, String> map = new HashMap<>();
        Student student1 = new Student("2022101", "张三");
        Student student2 = new Student("2022106", "李四");
        Student student3 = new Student("2022101", "张三");

        map.put(student1, "A207宿舍");
        map.put(student2, "A208宿舍");
        map.put(student3, "A208宿舍");

        System.out.println(student1.hashCode());
        System.out.println(student3.hashCode());

        System.out.println(student1.equals(student3));

        for (Map.Entry<Student, String> entry: map.entrySet()) {
            System.out.println(entry.getKey() + "分配在" + entry.getValue());
        }
    }
}

 

上面代码没有重写equals方法和hashCod方法,张三这个人被认为不是同一个人。只要重写了这两个方法,就会按照我们所想的方法进行。

 equals()方法:

 进入源码我们发现,它所比较的只是引用是否相等,这显然不符合我们Student类的情况。

hashCode()方法:

进入源码,它是一个native方法,也就是C/C++写的方法,我们无法继续看。

但通过其注释的一部分 

类Object的equals方法在对象上实现了最有区别的可能等价关系;也就是说,对于任何非空引用值x和y,当且仅当x和y引用同一对象时(x==y的值为true),此方法返回true。

请注意,通常需要在该方法被重写时重写hashCode方法,以便维护hashCod方法的一般约定,该约定规定相等的对象必须具有相等的哈希代码。

我们可以得知,它与equals方法是密不可分的。

那么我们重写后的这两个方法是在哪里发挥作用了呢?

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

class Student {
    public String id;
    public String name;

    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" + id + " "+ name + "}";
    }

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

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

}

public class TestHash {

    public static void main(String[] args) {
        HashMap<Student, String> map = new HashMap<>();
        Student student1 = new Student("2022101", "张三");
        Student student2 = new Student("2022106", "李四");
        Student student3 = new Student("2022101", "张三");

        map.put(student1, "A207宿舍");
        map.put(student2, "A208宿舍");
        map.put(student3, "A208宿舍");

        System.out.println(student1.hashCode());
        System.out.println(student3.hashCode());

        System.out.println(student1.equals(student3));

        for (Map.Entry<Student, String> entry: map.entrySet()) {
            System.out.println(entry.getKey() + "分配在" + entry.getValue());
        }
    }
}

 

 此时代码达到了我们预期效果。

调用不带参数的构造方法,第一次put()时才开辟内存:

equals和hashCode在部分源码中:

总结一下,在哈希桶中,hashCode()把数据定位到数组的哪个地方,而equals()在数组存放链表的头节点开始依次从hashCode()定位的地方往后找,比较数据是否相同,相同就替换value值,不进行插入,如都不相同,就插入新结点。

二者可以类比成查字典。按照拼音查字典,先找第一个目标拼音(类似于hashCode()一下),在具体找字(类似于equals()),相同读音的汉字虽然多,但是有了equals(),就可以区别出来。

 初始化空间大小:

初始化空间大小可能并不是表面上看起来那么简单。

 总结:当传入目标大小后,开辟的是2^n的值,同时要取到大于接近的值

 什么时候树化:

 总结:

数组长度 >= 64 && 链表长度 >= 8才可以树化 


有什么错误评论区指出,希望可以帮到你。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值