当前位置: 首页 > news >正文

哈希碰撞攻防战——深入浅出Map/Set的底层实现

各位看官早安午安晚安呀

如果您觉得这篇文章对您有帮助的话

欢迎您一键三连,小编尽全力做到更好
欢迎您分享给更多人哦

今天我们来学习Map/Set的底层实现

目录

问题一:hash会出现负数?数组越界

一:什么是二叉搜索树?

1.1:创建一个二叉搜索树

1.2.移除一个节点:

1.3.二叉搜索树的缺陷:

1.4.引出map和set以及和红黑树之间的关系

二:关于搜索我们一般用到的方式

2.1.静态查找(就只进行查找不进行其他的操作)

2.2.但是现实中的查找大多数都是动态查找:

2.3 模型

三:Map

3.1.关于Map.Entry (很重要)

3.2.Map常用方法说明

3.3.关于Map方法的注意事项

四:Set(去重很好)

4.2.Set注意事项

五:我们日常的比较

六:哈希表

6.1:哈希冲突

6.2:哈希冲突不可避免

6.2.1:冲突-避免-哈希函数设计

6.2.2. 冲突-避免-负载因子调节(重点掌握)

6.3:哈希冲突解决:

6.3.1.闭散列

6.3.2:开散列/哈希桶(特别重要,很好用)

6.4:泛型写法

一定要重写equals方法!!!

6.5:总结:性能分析

7. OJ练习

7.1:只出现一次的数字

7.2:随机链表的复制

7.3:宝石与石头

7.4:坏键盘打字

7.5:输出前K个高频单词


问题一:hash会出现负数?数组越界

问题二:为什么会死循环?

问题三:第一个练习:万一 一个数字出现了三次呢?

一:什么是二叉搜索树?

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树 :
  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树

1.1:创建一个二叉搜索树

public class BinarySearchTree {
    static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val) {
            this.val = val;
        }
    }

    public TreeNode root;

    public boolean search(int key){
        TreeNode cur = root;
        if(root == null){
            return false;
        }
        while(cur != null){
            if(cur.val > key){
                cur = cur.left;
            } else if (cur.val <key) {
                cur = cur.right;
            }else{
                return true;
            }
        }
        return false;
    }
    public boolean insert(int key){
        TreeNode node = new TreeNode(key);
        if(root == null){//  如果根就是的话,直接添加了,然后return true;
            root = node;
            return true;
        }
        TreeNode cur = root;
        TreeNode parent = cur;//赋值
        while(cur != null){
            if(cur.val < key){
                parent = cur;  //这一步一定要提前
                cur = cur.right;
            } else if (cur.val >key) {
                parent = cur;
                cur = cur.left;
            }else{
                return false;
            }
        }
        if(parent.val < key){   //你就说为什么cur == null了把它小于的话(cur在该添加的位置上面等于了null)
            parent.right = node;
        }else{
            parent.left = node;
        }
        return true;
    }

1.2.移除一个节点:

要思考的问题:

代码实现:

    public void remove(int val){
        TreeNode cur = root;//要移除这个节点,首先我要先遍历二叉搜索树找到这个节点
        TreeNode parent = null;
        if(root == null){
            return;
        }
        while(cur != null)
            if(cur.val < val) { 
                parent = cur;
                cur = cur.right;
            }else if(cur.val > val){
                parent = cur;
                cur = cur.left;
            }else{
                //要删除的节点,找到了
                removeTreeNode(cur,parent);
            }
    }

    private void removeTreeNode(TreeNode cur,TreeNode parent) {
        if(cur.left == null){
            if(cur.val > parent.val){//这一步就纯纯的是判读那cur走到了parent的左边还是右边,
                parent.right = cur.right;//你这一点都不知道,我滴妈你让parent的左边接力还是右边?
            }else{
                parent.left = cur.right;
            }
        }else if(cur.right == null){       //cur的右边空了
            if(cur.val > parent.val){//和上面一样
                parent.right = cur.left;
            }else{
                parent.left = cur.left;
            }
        }else{    // cur 的两边都不为空
            TreeNode target = cur.right; //准备
            TreeNode parentTarget = null;
            while(target.left != null){
                parentTarget = target;
                target = target.left;
            }
            cur.val = target.val;
            if(parentTarget != null){
                parentTarget.left = target.right;
            }else{
                cur.right = target.right;
            }
        }

    }

1.3.二叉搜索树的缺陷:

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
  • 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:
  • 最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2(这个时候效率已经没了)


1.4.引出map和set以及和红黑树之间的关系

TreeMap TreeSet java 利用搜索树实现的 Map Set ;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证。

二:关于搜索我们一般用到的方式

2.1.静态查找(就只进行查找不进行其他的操作)

1. 直接遍历,时间复杂度为 O(N) ,元素如果比较多效率会非常慢
2. 二分查找,时间复杂度为log(N), 但搜索前必须要求序列是有序的

2.2.但是现实中的查找大多数都是动态查找

1. 根据姓名查询考试成绩
2. 通讯录,即根据姓名查询联系方式
3. 不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,即动态查找,下面我将进行讲解Map和Set。 Map Set 是一种适合动态查找的集合容器(它们都是接口)。

2.3 模型

一般把搜索的数据称为关键字( Key ),和关键字对应的称为值( Value ),将其称之为 Key-value 键值对(具有映射关系),所以模型会有两种:
1. 纯 key 模型(TreeSet,HashSet)
比如:
有一个英文词典,快速查找一个单词是否在词典中,快速查找某个名字在不在通讯录中
2. Key-Value 模型(TreeMap,HashMap)
比如:
统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数: < 单词,单词出现的次数 >
Map 中存储的就是 key-value 的键值对, Set 中只存储了 Key

三:Map

Map官方文档

Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对(这个键值对往大了点说就是一个内部类)(譬如树里面的节点里面存储了左右孩子节点),并且K一定是唯一的,不能重复(那这个<K,V>)

3.1.关于Map.Entry<K, V>(很重要)

首先Entry<K,V>是Map接口里面的一个内部接口),自然,TreeMap里面的Entry<K,V>这个类也是实现了Entry这个内部接口

如图:TreeMap里面的Entry<K,V>这个类也是实现了Entry这个内部接口

关于Map.Entry<K,V>(在Map里面的内部接口)这个类就相当于二叉树里面的节点

总的来说Map.Entry<K, V> Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式

注意:Map.Entry<K,V>并没有提供设置Key的方法

3.2.Map常用方法说明

解释:在 Java 中,当你尝试将一个对象赋值给一个基本数据类型时,实际上会发生自动拆箱(unboxing)操作,但这之前还有一个更关键的检查点:空值检查。

在这个例子中,map.get("lihua") 返回一个 Integer 对象(或者 null,如果键 "lihua" 不存在)。但是,当你尝试将这个 Integer 对象赋值给一个 int 类型的变量 val 时,Java 编译器会插入一个自动拆箱操作。

然而,在自动拆箱之前,Java 运行时环境会检查 Integer 对象是否为 null。如果对象是 null,那么尝试进行拆箱操作(即将 Integer 转换为 int)是不合法的,因为基本数据类型 int 不能是 null。在这种情况下,Java 不会抛出类型转换异常(ClassCastException),而是会抛出一个空指针异常(NullPointerException)。

类型转换异常(ClassCastException)通常发生在尝试将一个对象强制转换为不兼容类型的对象时。但在这个例子中,问题不在于类型的不兼容,而在于尝试将一个空引用(null)用作基本数据类型的值。

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class Test {
    public static void main(String[] args) {

        Map<String,Integer> map = new TreeMap<>();
//1.put 和 get(Object key) 返回 key 对应的 value
        map.put("zhangsan",18);
        map.put("zhangsan",20);//会进行更新,key值不会重复
        map.put("lisi",18);
        map.put("wangWu",50);

        //int val = map.get("lihua");返回的是一个空指针异常而不是类型转换异常,很有讲究的
        //这个时候就可以用这个方法
        map.getOrDefault("lihua",20);// 没有key的话就相当于一次put,(key是lihua)(val是20)
//3.(keyset):Set<K> keySet() 返回所有 key 的不重复集合
        Set<String> set = map.keySet();  //经过排序的从大到小
        for (String s: set) {
            System.out.print(s + " ");
        }
        //或者
        System.out.println(set);
//4:Set<K> keySet() 返回所有 key 的不重复集合
        Collection<Integer> collection = map.values();//也是有顺序的,一般都是按照key的进行排序
        System.out.println(collection);
        System.out.println("==============");
//5:Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系
        Set<Map.Entry<String,Integer>> entries = map.entrySet();
        for (Map.Entry<String,Integer> entry:entries) {
            System.out.println("key:" + entry.getKey() + " val:" + entry.getValue());
        }
         //或者直接打印也行
        System.out.println(entries);


        System.out.println("===========");


    }

结果:

3.3.关于Map方法的注意事项

  • 1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
  • 2. Map中存放键值对的Key是唯一的,value是可以重复的
  • 3. TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常(在我看来放入TreeMap的key需要进行比较利用compareTo方法,(这个时候null调用compareTo方法就会报空指针异常)所以不能插入key为空的情况)(TreeMap是基于红黑树实现的,它要求所有的key都必须实现Compare接口,或者通过构造函数传入Comparator。value可以为空。但是HashMapkeyvalue都可以为空。
  • 4. Map中的Key可以全部分离出来,存储到Set中(Set<K> keySet()来进行访问(因为Key不能重复)
  • 5. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)
  • 6. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。

四:Set(去重很好)

Set的官方文档

4.2.Set注意事项

注意:

  • 1. Set是继承自Collection的一个接口类
  • 2. Set中只存储了key,并且要求key一定要唯一
  • 3. TreeSet的底层是使用Map来实现的,其使用keyObject的一个默认对象作为键值对插入到Map中的
  • 4. Set最大的功能就是对集合中的元素进行去重
  • 5. 实现Set接口的常用类有TreeSetHashSet,还有一个LinkedHashSetLinkedHashSet是在HashSet的基础
  • 上维护了一个双向链表来记录元素的插入次序。
  • 6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
  • 7. TreeSet中不能插入nullkey(和TreeMap的解释一样,都是要进行比较的)HashSet可以。

五:我们日常的比较

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经过关键 码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O(
) ,搜索的效率取决于搜索过程中元素的比较次数。
那有没有一种理想的搜索方式使得我们直接就能找到呢?
函数:数学函数!!!给一个x就能得出一个y,时间复杂度就是O(1)

至此,我们成功引入哈希表!!!

六:哈希表

插入元素:我们通过对关键码(Key)的运算,计算出该元素存储的位置,然后直接放在该位置

搜索元素:对元素的Key进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称为哈希表 (Hash Table)( 或者称散列表 )
例如:数据集合 {1 7 6 4 5 9}
哈希函数设置为 hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小

但我再加一个为44的元素呢?结果就是完蛋,和4这个元素你俩撞上了

6.1:哈希冲突

上述的两个元素撞上了,就是你俩通过一个哈希函数然后计算出的哈希值一模一样,导致你俩放的位置一样,这样就是大名鼎鼎的——哈希冲突

具体解释:

对于两个数据元素的关键字 和 (i != ) ,有k i != kj  ,但有: Hash( ki) == Hash(kj) ,即: 不同关键字通过相同哈 希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞
把具有不同关键码而具有相同哈希地址的数据元素称为 同义词

6.2:哈希冲突不可避免

首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的 ,但我们能做的应该是尽量的 降低冲突
那么我们用该如何尽量避免冲突呢?
  1. 设计合理的哈希函数
  2. 扩容(和负载因子有关):底层数组尽量可以根据放入元素的多少进行扩容(底层容量改变了,冲突也会尽量少一点)

6.2.1:冲突-避免-哈希函数设计

哈希函数设计不够合理 哈希函数设计原则

       1.  哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间(我是数组我要连续(*^▽^*))

      2.  哈希函数计算出来的地址能均匀分布在整个空间中

      3.哈希函数应该比较简单

常见的哈希函数(有的大家只需要了解就行了)

1. 直接定制法 --( 常用 )
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况
面试题:只出现一次 的字符
2. 除留余数法 --( 常用 )
设散列表中允许的地址数为m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:
Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
3. 平方取中法 --( 了解 )
假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址; 再比如关键字为 4321 ,对它平方就是18671041 ,抽取中间的 3 671( 710) 作为哈希地址 平方取中法比较适合:不知道关键字的分 布,而位数又不是很大的情况
4. 折叠法 --( 了解 )
折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法 --( 了解 )
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中 random 为随机数函数。
通常应用于关键字长度不等时采用此法
6. 数学分析法 --( 了解 )
设有 n d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转( 如1234改成 4321) 、右环位移 ( 1234 改成 4123) 、左环移位、前两数与后两数叠加 ( 1234 改成 12+34=46) 等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均 匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

6.2.2. 冲突-避免-负载因子调节(重点掌握)

小编用两张图来进行讲解

6.3:哈希冲突解决:

一般是这两种方法

  1. 二次探测和线性探测(闭散列 让你们两个冲突的元素,第一个放在计算出来的位置,然后再给另一个元素再设计一个函数进行放置(也称之为二次探测),  不进行二次探测的话,也可以把另外一个元素进行线性探测(既然这个位置已经有人了,那我就放在下一个空的位置(反正小编不想这么干))
  2. 数组 + 链表开散列/哈希桶(小编很喜欢这个):既然你俩计算出来相同的位置了,你俩都别委屈了,都来吧(

等等方法………………(接下俩小编为大家讲解这几个方法)

6.3.1.闭散列

我们来想想

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 key 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置呢?
带着这个问题我们迎来两个解决办法
解决一: 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

线性探测的缺点: 线性探测会导致产生冲突的数据堆积在一块,这与其找下一个空位置有关系(一个一个挨着找,这种情况很正常)而二次探测可以避免这个问题

解决二:二次探测:

找下一个空位置的方法为:Hi = (H0 + i^2) % m,或者   :   Hi = (H0 + i^2) % m其中:i = 1,2,3… , H0是通过散列函数Hash(x) 对元素的关键码 key 进行计算得到的位置,m是表的大小(数组的长度)。 如果要插入 44 ,和一开始的元素4产生冲突,使用解决后的情况为:

研究表明:当表的长度为质数且表装载因子 a 不超过 0.5 时9(在这个情况下),新的元素一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a 不超过 0.5 ,如果超出必须考虑增容。
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

6.3.2:开散列/哈希桶(特别重要,很好用)

开散列/哈希桶(数组 + 链表)

虽然这样可行,但是万一冲突的元素太多了怎么办呢?(我还不是要遍历链表?)

好好好,那我就把你链表变成二叉树

这个思想就是不断地把(大集合的搜索问题转化为小集合的搜索问题

6.3.3:开散列/哈希桶的实现

// key-value 模型
public class HashBucket {
    public static class Node {
        private int val;
        private int key;
        public Node next;


        //这里一定要注意,你如果在这里上来就构造了一个根节点的
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }
    private static final double LOAD_FACTOR = 0.75;//负载因子
    //根本不需要根节点,我底层是数组进行实现的,要你没有用
    //public Node root;
    public Node[] array = new Node[5];    //我这个就是HsahMap的底层实现,开散列:数组 + 链表

    private int usedSize;   //一般只要一个数据结构底层运用了数组,一般都会有usedSize去记录有效元素
    public boolean search(int key) {   //这个要判断val吗?
        int index = key % array.length;
        Node cur = array[index];//   我直接计算出这个key应该在的位置,然后在数组的这个位置进行查找
        while (cur != null) {
            if (cur.key == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    public void put(int key, int val) {
        int index = key % array.length;
        Node cur = array[index];
        while (cur != null) {  // 先遍历一遍这个链表,判读这个key在不在里面,如果在的话,就更新val值
            if (cur.key == key) {
                cur.val = val;
            }
            cur = cur.next;
        }
        //走到这里说明不在这里,直接进行头插
        Node node = new Node(key,val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
        if(usedSize * 1.0/ array.length > LOAD_FACTOR){  //这里要乘  1.0
           revise(array);
        }
    }

    private void revise(Node[] array) {  //这里要是有参数的话,下面的array,一定要加this啊!!!!!!!!!!!
        //首先让数字进行扩容
        //最后再返回数组
        Node[] tmpArray = Arrays.copyOf(array,2*array.length);
        //先遍历一遍原来的数组,再在每个数组元素上遍历链表
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while(cur != null){
                //计算出新的下标,再进行头插

                int newIndex = cur.key % tmpArray.length;
                //这个节点就是cur其实没必要再多定义了
                /*Node node = new Node(cur.key,cur.val);
                node.next = tmpArray[newIndex];
                tmpArray[newIndex] = node;
                cur = cur.next;
*/
                //你们是一样的道理
                Node curNext = cur.next;
                cur.next = tmpArray[newIndex];
                tmpArray[newIndex] = cur;
                cur =curNext;
            }
        }
        array = tmpArray;//   这里一定要this啊!!!!!!!!!!!!!
        //  除非上面没有参数
    }
    public int get(int key){
        int index = key % array.length;
        Node cur = array[index];//   我直接计算出这个key应该在的位置,然后在数组的这个位置进行查找
        while (cur != null) {
            if (cur.key == key) {
               return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }
}

Test(测试)

public class Test {
    public static void main(String[] args) {
        HashBucket hashBucket = new HashBucket();
        hashBucket.put(1,1);
        hashBucket.put(2,2);
        hashBucket.put(3,3);
        hashBucket.put(4,4);
        hashBucket.put(5,5);//就这里i = 1下标出问题了
        System.out.println("===============");

        System.out.println(hashBucket.get(1));
        System.out.println(hashBucket.get(2));
        System.out.println(hashBucket.get(5));
        System.out.println(hashBucket.get(9));

    }
}

我这里给大家提出一个问题,如果我上述有revise(array)并且后面array = tmpArray,(我也没加this,为什么最后会死循环呢?)

大家可以去运行一下,为什么最后会死循环?

6.4:泛型写法

class Person{
    String name;

    public Person(String name) {
        this.name = name;
    }
    //一定要重写equals方法,因为我要看看你一开始在不在里面,相同名字的lihua
    @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(name, person.name);
    }
   //这个也是
    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

public class HashBucket1 <K,V>{
    public class Node<K,V>{  //为什么不能是静态的?(静态的不依赖于类,所以说我根本不知道K,V是谁?
        private K key;
        private V val;
        private Node<K,V> next;


        //这里一定要注意,你如果在这里上来就构造了一个根节点的
        public Node( K key, V val) {
            this.key = key;
            this.val = val;
        }
    }
    private Node[] array ;
    private static final double LOAD_FACTOR = 0.75;//负载因子


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

    private int usedSize;
    public void put(K key, V val){
        int hash = key.hashCode();
        //还能是负的?
        int index = Math.abs(hash % array.length);
        Node<K,V> cur = array[index];
        //先找一遍看有没有,没有直接头插法
        while(cur != null){
            if(cur.key.equals(key)){   //所以说这个key一定要重写equals方法
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        // 到这里就肯定没有了,没有直接头插法
        Node<K,V> node = new Node<>(key,val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
        // 判断负载因子大不大
        if(usedSize*1.0 / array.length > LOAD_FACTOR){
            revise();
        }
    }

    private void revise() {
        Node<K,V> [] tmpArray = Arrays.copyOf(array,2*array.length);
        for (int i = 0; i < array.length; i++) {
            Node<K,V> cur = array[i];
            while(cur != null){
                Node<K,V> curNext = cur.next;
                //找到新下标直接进行插入
                int newIndex = Math.abs(cur.hashCode() % tmpArray.length);
                cur.next = tmpArray[newIndex];
                tmpArray[newIndex] = cur;
                cur = curNext;
            }
        }
        this.array = tmpArray;
    }
    public V get(K key){
       int index = key.hashCode() % array.length;
       Node<K,V> cur = array[index];
       while(cur != null){
           if(cur.key == key){
               return cur.val;
           }
           cur = cur.next;
       }
       return null;
    }
    public boolean search(K key){
        int index = key.hashCode() % array.length;
        Node<K,V> cur = array[index];
        while(cur != null){
            if(cur.key == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
}

Test测试

public class TestHashBucket1 {
    public static void main(String[] args) {
        Person person = new Person("zhangSan");
        Person person1 = new Person("zhangSan");
        Person person2 = new Person("zhangSan");
        HashBucket1<Person,Integer> hashBucket1= new HashBucket1();
        //这个key一定要重写equals方法(不然俺咋判断,我写的哈希表里面有没有这个元素
        hashBucket1.put(person,18);
        hashBucket1.put(person1,18);
        hashBucket1.put(person2,18);
        System.out.println("============");

    }
}

一定要重写equals方法!!!

写了:

没写:

6.5:总结:性能分析

虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入 / 删除 / 查找时间复杂度是 O(1)
5.12 java 类集的关系
  1.  HashMap HashSet java 中利用哈希表实现的 Map Set
  2.  java 中使用的是哈希桶方式解决冲突的
  3.  java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
两个方法:(hasCode和equals方法)
 java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key equals 方法。所以如果要用自定义类作为 HashMap key 或者 HashSet 的值, 必须重写  hashCode equals 法。
而且要做到 equals 相等的对象, hashCode 一定是一致的。

7. OJ练习

7.1:只出现一次的数字

只出现一次的数字

方法一:我们没学哈希表之前(直接全部异或就好了)

方法二:

class Solution {
    public int singleNumber(int[] nums) {
       Set<Integer> set = new HashSet<>();
//一边进去,相同的出来
        for(int i = 0; i < nums.length; i++){
           if(set.contains(nums[i])){
            set.remove(nums[i]);
           }else{
            set.add(nums[i]);
           }
        }
//如果包含就是你了(只出现过一次)
         for(int i = 0; i < nums.length; i++){
           if(set.contains(nums[i])){
            return nums[i];
           }
        }
        return -1;
    }
}


7.2:随机链表的复制

随机链表的复制

import java.util.Map;
class Solution {
    public Node copyRandomList(Node head) {
        Node cur = head;
        Map<Node,Node> map = new HashMap<>();
        while(cur != null){
            Node node = new Node(cur.val);
            map.put(cur,node);   //帮下面的链接上,通过HashMap
            cur = cur.next;
            //node = node.next;       这样写肯定不对,因为我每次都要new一个Node对象,就链接不上了
            
        }
        cur = head;
        
        while(cur != null){
            map.get(cur).random = map.get(cur.random);
            map.get(cur).next = map.get(cur.next);
            cur = cur.next;
        }
        return map.get(head);
    }
}

7.3:宝石与石头

宝石与石头

class Solution {
    public int numJewelsInStones(String jewels, String stones) {
        Set<Character> set = new HashSet<>();
        for(int i = 0; i<jewels.length(); i++){
            set.add(jewels.charAt(i));   //    是add而不是put
        }
    int count = 0;
        for(int i = 0; i<stones.length(); i++){
            char ch = stones.charAt(i);
            if(set.contains(ch)){
                count++;
            }
        }
        return count;
    }
}

7.4:坏键盘打字

坏键盘打字

 public static void find(String s1,String act){
        Set<Character> set = new HashSet<>();
        for (char ch: act.toUpperCase().toCharArray()) {
            set.add(ch);
        }
        Set<Character> set2 = new HashSet<>();
        for (char ch: s1.toUpperCase().toCharArray()) {
            if(! set.contains(ch)){
                set2.add(ch);
            }
        }
        for (Object o:
             set2.toArray()) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {
        String s1 = "abc def 7 abc";
        String s2 = "abc";
        find(s1,s2);
    }

需要掌握,String的toUpperCase()(转换大写)toCharArray()(转换成字符数组)

以及Set的toArray()将里面的元素转换成数组

7.5:输出前K个高频单词

输出前K个高频单词

public  List<String> topKFrequent(String[] words, int k) {
        // 一个哈希表,统计所有单词出现的个数
        Map<String,Integer> map = new HashMap<>();
        for(String word : words){
            if( !map.containsKey(word)){
                map.put(word,1);
            }else{
                int count =  map.get(word);
                map.put(word,++count);
            }
        }
        //建立小根堆,重写比较方法
        //  这里的Entry<>记得前面加上Map,因为他是Map里面的内部类,所有的都要加
        PriorityQueue<Map.Entry<String,Integer>> minHeap = new PriorityQueue<>(new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                if((o1.getValue() - o2.getValue()) == 0 ){
                    return o2.getKey().compareTo(o1.getKey());
                }
                return o2.getValue() - o1.getValue();
            }

        });
        for (Map.Entry<String,Integer> entry : map.entrySet()) {
            Map.Entry<String,Integer> top =  minHeap.peek();
            if(minHeap.size() <= k){
                minHeap.offer(entry);
            }else{
                //   如果你俩的出现的频率相同,让字母的ASCII码值进行比较大的进来
                if(top.getValue().equals(entry.getValue())){
                    if(top.getKey().compareTo(entry.getKey()) > 0){
                        minHeap.poll();
                        minHeap.offer(entry);
                    }
                }else{
                    // 下一个元素的val大,让下一个元素进来
                    if(entry.getValue().compareTo(top.getValue()) > 0){
                        minHeap.poll();
                        minHeap.offer(entry);
                    }
                }
            }
        }
        List<String> ret = new ArrayList<>();
        for (int i = 0; i < k; i++) {
            Map.Entry<String,Integer> top = minHeap.poll();
            ret.add(top.getKey());
        }
        Collections.reverse(ret);
        return ret;
    }

上述就是哈希碰撞攻防战——深入浅出Map/Set的底层实现

的全部内容了,哈希碰撞还有很多我们的未解之谜,让未来的我们一起去解决吧~~~

能看到这里相信您一定对小编的文章有了一定的认可。

有什么问题欢迎各位大佬指出
欢迎各位大佬评论区留言修正~~

您的支持就是我最大的动力​​​!!!!

相关文章:

  • 2025.3.2机器学习笔记:PINN文献阅读
  • uniapp 系统学习,从入门到实战(七)—— 网络请求与数据交互
  • 多镜头视频生成、机器人抓取、扩散模型个性化 | Big Model weekly第58期
  • (KTransformers) RTX4090单卡运行 DeepSeek-R1 671B
  • 探索紧急灾难处理的智慧:基于Neo4j的知识图谱问答系统
  • 【XSS】DVWA靶场XSS攻击
  • 进度条 —— 第一个linux程序
  • 我更新啦!纯手工编写C++画图,有注释!
  • 【JavaEE】-- 多线程(初阶)3
  • DeepSeek-R1 如何凭联网优势登顶智创聚合 API 模型使用榜首
  • Windows 10 远程桌面连接使用指南
  • 分光器的光衰计算公式。
  • Angular Loss论文理解
  • 特别呈献:AIGC生图超现实VR全景特辑
  • 【算法】3302. 表达式求值
  • 如何在 ArcGIS Pro 中将SHP转为KML:详细步骤与操作指南
  • 如何把图片或者图片地址存到 MySQL 数据库中以及如何将这些图片数据通过 JSP 显示在网页中
  • 2 Redis 字符串(String) 命令大全
  • ISP CIE-XYZ色彩空间
  • Keil5 MDK使用记录
  • 上海网站建设代码/百度推广注册
  • 酷站网官网/完整的网页设计代码
  • 织梦做的网站不能用手机访问/网站推广和优化的原因网络营销
  • 做免费网站教程/福建seo排名培训
  • 深圳龙华街道三联社区/太原seo网站管理
  • 阿里巴巴国际站靠谱吗/百度快速查询