• 数据结构Map-Set和哈希表


    目录

    概念

    模型

    Map

    Map的常用方法

    对于Map的总结

    Set

    Set的常见方法

    关于Set的总结

    哈希表

    概念

    冲突

    概念

    哈希函数设计原则

    常见的哈希函数

    1.直接定制法(常用)

    2.除留余数法(常用)

    3.平方取中法

    4.折叠法

    5.随机数法

    6.数学分析法

    冲突避免-负载因子调节

    冲突-解决

    闭散列

    线性探测

    二次探测

    开散列/哈希桶

    三个例题理解Map和Set

    复制带随机指针的链表


    概念

    Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关,比如TreeMap和TreeSet的效率一般是不如HashMap和HashSet的.

    模型

    一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称为Key-value的键值对,所以对应的模型会有两种:纯Key模型和Key-Value模型.

    Map中存储的就是key-value模型,Set中只存储了Key.

    Map

    Map是一个接口类,该类没有继承自Collection,该类中存储的是结构的键值对,而且K一定是唯一的,不能重复.

    TreeMap实现了SortedMap接口,所以TreeMap中的元素一定是可比较的.

    Map的常用方法

    方法演示

    1. TreeMap map = new TreeMap<>();
    2. map.put("hello",2);
    3. map.put("abc",4);
    4. //返回key对应的value
    5. //如果map中没有hello,会返回null
    6. Integer e =map.get("hello");
    7. System.out.println(e);
    8. //有key返回对应值,没有返回默认值
    9. Integer s = map.getOrDefault("hello2",520);
    10. System.out.println(s);
    11. //取出key值,进行组织
    12. //用Set接收
    13. Set set = map.keySet();
    14. System.out.println(set);
    15. //取出value值,进行组织
    16. //用Collection接收
    17. Collection collection = map.values();
    18. System.out.println(collection);
    19. //containsKey,判断是否包含key
    20. boolean t = map.containsKey("hello");
    21. System.out.println(t);
    22. //containsValue,判断是否包含value
    23. boolean r = map.containsValue("88");
    24. System.out.println(r);

    entrySet方法

    Map.Entry是Map内部实现的用来存放键值对映射关系的内部类,该内部类中主要提供了下列方法:

    1. Set> entrySet = map.entrySet();
    2. for (Map.Entry entry : entrySet) {
    3. System.out.println("Key: " + entry.getKey() +" value: " + entry.getValue());
    4. }

    此方法就是将<"hello",2>作为一个整体存放在set当中,这个整体的类型Map.Entry.

    由于Map并没有实现Iterable接口,所以for-each无法直接遍历Map.

    这个方法相当于是提供了遍历Map的方法.


    对于Map的总结

    1. Map 是一个接口,不能直接实例化对象 ,如果 要实例化对象只能实例化其实现类 TreeMap 或者HashMap
    2. Map 中存放键值对的 Key 是唯一的, value是可以重复的.因为Map的底层是搜索树(红黑树).
    3. Map 中的 Key 可以全部分离出来,存储到 Set 来进行访问 ( 因为 Key 不能重复 )。
    4. Map 中的 value 可以全部分离出来,存储在 Collection 的任何一个子集合中 (value 可能有重复 )。
    5. Map中键值对的 Key 不能直接修改, value 可以修改,如果要修改 key ,只能先将该 key删除掉,然后再来进行重新插入。

    Set

    Set 与Map的不同主要有两点:Set是继承自Collection的接口类,Set中只存储了Key.

    Set的常见方法

    在TreeSet当中存储元素的时候,其实是存在了TreeMap当中,但是value是默认的一个值.

    不管存哪个key,value永远是PRESENT这个值.

    关于Set的总结

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

    哈希表

    以往,我们在一些记录当中查找指定数据的时候,会有以下几种方法

    1. 把数据存储到数组当中,然后遍历数组去查找.时间复杂度是O(N)
    2. 假设数据是有序的情况下,那么二分查找是最快的,时间复杂度可以达到O(lognN).
    3. 我们也可以利用搜索树,可以达到O(logN).

    现在,有一种可以将时间复杂度做到O(1)的结构,那就是哈希表.

    概念

    不经过任何比较,一次直接从表中得到要搜索的元素.如果构造一种存储结构,通过某种函数使元素的存储位置和它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快的找到该元素.

    该港是即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表(HashTable)(散列表).

    例如:数据集合{176459}

    哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。

    用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快.但是,如果以此方式,向集合里插入14,会出现位置占用的问题,这就出现了冲突.

    冲突

    概念

    不同关键字通过哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或者哈希碰撞.

    把具有不同关键码而具有相同哈希地址的数据元素称为"同义词".

    冲突避免-巧妙设计哈希函数

    我们要明确,由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致冲突的发生是必然的,我们能做的应该是尽量的降低冲突率.

    哈希函数设计原则

    引起哈希冲突的一个原因可能是:哈希函数设计不合理.

    设计原则:

    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0m-1之间
    • 哈希函数计算出来的地址能均匀分布在整个空间中
    • 哈希函数应该比较简单
    常见的哈希函数
    1.直接定制法(常用)
    取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况.
    例题
    直接定制法例题
    1. class Solution {
    2. public int firstUniqChar(String s) {
    3. int[] array = new int[26];
    4. for(int i = 0; i < s.length();i++){
    5. char ch = s.charAt(i);
    6. array[ch-'a']++;
    7. }
    8. for(int i = 0; i < s.length();i++){
    9. char ch = s.charAt(i);
    10. if(array[ch-'a'] == 1){
    11. return i;
    12. }
    13. }
    14. return -1;
    15. }
    16. }
    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) 等方
    法。
    数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均 匀的情况.

    冲突避免-负载因子调节

    散列表的载荷因子定义为:α =填入表中的元素个数/散列表的长度.

    α 是散列表装满程度的标志因子.由于表长是定值,α 与填入表中的元素个数成正比,α 越大,表明填入表中的元素越多,产生冲突的可能性就越大.

    负载因子和冲突率的关系演示

    当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率.

    已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组大小.

    α 与哈希表数组的长度是成反比的.


    冲突-解决

    解决哈希冲突的两种常见方式:闭散列和开散列.

    闭散列

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

    寻找下一个空位置有两种方法:线性探测法和二次探测法.

    线性探测

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

    比如要插入44,先通过哈希函数获取待插入元素在哈希表中的位置,如果该位置没有元素就直接插入新元素,显然这里4占据了位置;此时元素发生哈希冲突,则使用线性探测法找到下一个空位置,下标为8的位置,插入44.

    需要注意的是,采用此种方法处理哈希冲突的时候,不能随便物理删除哈希表中已有的元素,若直接删除会影响其他元素的搜索.比如删除4,如果直接删除4,那么对查找44就会产生影响.因此线性探测采用标记的伪删除法来删除一个元素.

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

    开散列/哈希桶

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

    开散列的每个桶中放的都是发生哈希冲突的元素.开散列可以认为是把一个在大集合中的搜索问题转化为在小集合中搜索了.

    开散列采取数组+链表的方式来组织数据.当数组长度超过64并且链表长度超过8的时候,链表就会变为红黑树.

    JDK1.7及以前,链表的插入采取的是头插法,JDK1.8开始采用尾插法.

    开散列法也是Java中用来解决哈希冲突所采取的方法.


    三个例题理解Map和Set

    1. //统计10w个数据中,不重复的数据(去重)
    2. public static void func1(int[] array) {
    3. HashSet set = new HashSet<>();
    4. for (int i = 0; i < array.length; i++) {
    5. set.add(array[i]);
    6. }
    7. System.out.println(set);
    8. }
    9. //2、统计10W个数据当中,第一个重复的数据?
    10. public static void func2(int[] array){
    11. HashSet set = new HashSet<>();
    12. for (int i = 0; i < array.length; i++) {
    13. if (!set.contains(array[i])){
    14. set.add(array[i]);
    15. }else {
    16. System.out.println(array[i]);
    17. return;
    18. }
    19. }
    20. }
    21. //3、统计10W个数据当中,每个数据出现的次数? 对应的关系
    22. public static void func3(int[] array){
    23. HashMap map = new HashMap<>();
    24. for (int i = 0; i < array.length; i++) {
    25. //第一次存入
    26. if (map.get(array[i]) == null){
    27. map.put(array[i],1);
    28. }else {
    29. int val = map.get(array[i]);
    30. map.put(array[i],val+1);
    31. }
    32. }
    33. Set> set = map.entrySet();
    34. for (Map.Entry entry:set) {
    35. System.out.println(entry.getKey() + "出现了" + entry.getValue()+"次");
    36. }
    37. }

    复制带随机指针的链表

    用Map去做,会变得非常容易.

    1. /*
    2. // Definition for a Node.
    3. class Node {
    4. int val;
    5. Node next;
    6. Node random;
    7. public Node(int val) {
    8. this.val = val;
    9. this.next = null;
    10. this.random = null;
    11. }
    12. }
    13. */
    14. class Solution {
    15. public Node copyRandomList(Node head) {
    16. //用Map存储新老节点的对应关系
    17. HashMap map = new HashMap<>();
    18. Node cur = head;
    19. while(cur != null){
    20. Node node = new Node(cur.val);
    21. map.put(cur,node);
    22. cur = cur.next;
    23. }
    24. cur = head;
    25. while(cur != null){
    26. map.get(cur).next = map.get(cur.next);
    27. map.get(cur).random = map.get(cur.random);
    28. cur = cur.next;
    29. }
    30. return map.get(head);
    31. }
    32. }

    两个对象的hashcode一样,equals不一定一样.

    两个对象的equals一样,hashcode一定一样.

    java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key equals 方 法。所以如果要用自定义类作为 HashMap key 或者 HashSet 的值, 必须覆写 hashCode equals ,而且要做到 equals 相等的对象, hashCode 一定是一致的。

  • 相关阅读:
    在 SQL 中计算分页元数据,无需额外的往返
    pmp项目管理考试是什么?适合哪些人学?
    html表单元素
    无损分区工具对电脑硬盘分区,介绍一款硬盘无损分区工具
    13个Webpack 优化技巧
    服务端apk打包教程
    图片上传裁剪react-cropper
    同时安装python2和3解决方案
    观察者模式Java示例代码
    设计模式之建造者模式
  • 原文地址:https://blog.csdn.net/m0_62360856/article/details/132811416