• 【数据结构】哈希表


    目录

    一、概念

    二、哈希冲突

    三、避免冲突-哈希函数

    3.1 哈希函数设计原则

    3.2 常见的哈希函数

    3.2.1 直接定制法

    3.2.2 除留余数法

    3.2.3 其他

    四、避免冲突-负载因子

    五、解决冲突-闭散列

    5.1 线性探测

    5.2 二次探测

    六、解决冲突-开散列(哈希桶)

    七、手动实现HashMap

    7.1 手动实现HashMap(key的类型是int)

    7.2 手动实现HashMap(key的类型是引用类型)

    7.3 hashcode和equals的作用

    八、HashMap源码解读

    8.1 HashMap的默认容量是16

    ​8.2 HashMap的最大容量:1<<30

    8.3 HashMap的容量必须为2的倍数

    8.4 HashMap的默认负载因子是0.75

    8.5 HashMap有四种构造方法

    8.6 HashMap中的单链表变红黑树的条件

    九、常见的考题


    一、概念

    在顺序结构以及平衡树中,因为元素与存储位置之间没有对应的关系。因此在查找一个数据时,需要经过多次的比较。

    理想的搜索方法:不经过任何比较。一次性从表中得到要搜索的数据。

    解决方案:哈希表

    通过某种函数使得元素和元素的存储位置建立一一映射的关系

    在插入元素时:根据元素和函数,计算出存储的位置进行存储

    查找元素:根据元素和函数,求出存储的位置

    这种方法,不是十全十美,会存在哈希冲突问题。

    二、哈希冲突

    哈希冲突:两个元素通过哈希函数计算得到相同的哈希地址

    哈希冲突是没有办法避免的。能做的是降低冲突率

    解决思路有两种:

    • 尽可能的避免哈希冲突:设计合适的哈希函数、负载因子
    • 冲突发生后尽可能的解决冲突:闭散列、哈希桶

    三、避免冲突-哈希函数

    3.1 哈希函数设计原则

    • 哈希函数的定义域必须包含要存储的全部关键码
    • 哈希函数计算出来的地址能均匀分布在整个空间中
    • 哈希函数比较简单

    3.2 常见的哈希函数

    3.2.1 直接定制法

    取关键字的某个线性函数为地址: Hash (Key) = A*Key+B

    优点:简单、均匀

    缺点:需要事先知道关键字的分布情况

    使用场景:适合查找比较小且连续的情况

    3.2.2 除留余数法

    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

    3.2.3 其他

    平方取中、折叠法、随机数法、数学分析法

    四、避免冲突-负载因子

    负载因子=填入表中的元素个数 / 散列表的长度

     负载因子越大,冲突率越高。因为需要存储的关键字的个数不能改变,所以要降低负载因子,只能调整哈希表中的数组的大小。

    五、解决冲突-闭散列

    当发生哈希冲突时,如果哈希表没有装满。可以将key放到冲突位置的下一个空位置去。

    5.1 线性探测

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

    要插入44,4这个位置已经被占用,从4这个位置往后找,知道找到一个空位置 

     缺点:

    1. 采用这种方法处理哈希冲突,不能随便物理删除哈希表中的元素。因为会影响其他元素的查找。删掉4,44也就找不到了。删除采用标记的伪删除法来删除一个元素。
    2. 冲突的数据会堆积在一块。

    5.2 二次探测

    寻找空位置的方法:

    i为第i次冲突。

      开散列最大的缺点就是空间利用率低。

    六、解决冲突-开散列(哈希桶)

    将相同地址的关键码归于同一个集合,每一个子集称为一个桶,各个桶中的元素通过一个单链表链接起来,将各链表的头节点存储在哈希表中。

    JDK1.7以前是头插法,从1.8开始,是尾插法。 

    七、手动实现HashMap

    7.1 手动实现HashMap(key的类型是int)

    实现原理:

    底层:数组+单链表

    哈希函数:hash(key) = key%数组长度

    负载因子:0.75

    1. class HashBuck{
    2. static class Node{
    3. public int key;
    4. public int val;
    5. public Node next;
    6. public Node(int key,int val){
    7. this.key = key;
    8. this.val = val;
    9. }
    10. }
    11. public Node[] array;
    12. public int usedSize; // 已经存放的元素的个数
    13. public HashBuck(){
    14. this.array = new Node[4];
    15. }
    16. public int get(int key){
    17. // 找位置
    18. int index = key % array.length;
    19. // 遍历当前index下标的链表
    20. Node cur = array[index];
    21. while (cur!= null){
    22. if(cur.key == key){
    23. return cur.val;
    24. }else{
    25. cur = cur.next;
    26. }
    27. }
    28. return -1; // 找不到的情况
    29. }
    30. // 采用尾插法
    31. public void put(int key,int value){
    32. // 先计算下标
    33. int index = key % array.length;
    34. Node node = new Node(key,value);
    35. Node cur = array[index];
    36. Node pre = null;
    37. // 判断index下标是否有值
    38. if(cur == null){
    39. array[index] = node;
    40. }else{
    41. // 遍历当前index下标,判断是否有相同key
    42. while (cur != null) {
    43. // 遍历的时候判断,如果key这个位置已经有值,则需要进行覆盖
    44. if(cur.key == key){
    45. cur.val = value;
    46. }else{
    47. pre = cur;
    48. cur = cur.next;
    49. }
    50. }
    51. pre.next = node;
    52. }
    53. this.usedSize++;
    54. if(loadFactor() >= 0.75){
    55. // 扩容的问题:重新哈希 每个下标的每个列表的每个节点
    56. resize();
    57. }
    58. }
    59. public double loadFactor(){
    60. return this.usedSize *1.0 /array.length;
    61. }
    62. public void resize(){
    63. Node[] newArray = new Node[array.length*2];
    64. for (int i = 0; i < array.length; i++) {
    65. // 得到i下标的节点
    66. Node cur = array[i];
    67. while (cur!= null){
    68. // 将节点放入新数组的链表时,需要将next置null。为了保证可以遍历,所以需要将cur.next保存下来
    69. Node curNext = cur.next;
    70. // 计算在新数组的位置
    71. int index = cur.key% newArray.length;
    72. Node curNew = newArray[index];
    73. // 如果该位置上没有元素,直接放
    74. if(curNew == null){
    75. newArray[index] = cur;
    76. }else{
    77. // 有的话,尾插法
    78. while (curNew.next != null){
    79. curNew = curNew.next;
    80. }
    81. curNew.next = cur;
    82. }
    83. // 因为节点放到新的链表,所以之前链表的顺序已经不成立,所以需要将cur.next=null
    84. cur.next = null;
    85. cur = curNext;
    86. }
    87. }
    88. }
    89. }
    90. public class TestDemo {
    91. public static void main(String[] args) {
    92. HashBuck hashBuck = new HashBuck();
    93. hashBuck.put(0,11);
    94. hashBuck.put(1,111);
    95. hashBuck.put(2,12);
    96. hashBuck.put(4,13);
    97. System.out.println(hashBuck);
    98. }
    99. }

    7.2 手动实现HashMap(key的类型是引用类型)

    在7.1中,采用的哈希函数是 hash(key) = key%数组长度。如果此时的key不是int,而是String,就不能计算元素的存储位置。将问题总结为:如果key不是int类型,需要将key转为数字,才能使用哈希函数计算位置。

    解决方案:Object类的hashcode()方法,可以将传来的对象转为一个合法的数字。

    新建一个Student类。假设key是student对象,为了能够将student对象转为数字,Student类需要重写hashcode()函数。在put元素时,涉及到对象的比较,所以需要重写equals方法。

    1. class Student{
    2. public String id;
    3. public Student(String id){
    4. this.id = id;
    5. }
    6. @Override
    7. public int hashCode() {
    8. return super.hashCode();
    9. }
    10. @Override
    11. public boolean equals(Object obj) {
    12. return super.equals(obj);
    13. }
    14. }

    与7.1中代码不同的是:

    • 使用hashcode()将key转为数字
    • 比较key、value是否相等,采用的是equals方法
    1. public class HashBuck {
    2. static class Node{
    3. public K key;
    4. public V value;
    5. public Node next;
    6. public Node(K key,V value){
    7. this.key = key;
    8. this.value = value;
    9. }
    10. }
    11. public Node[] array = new Node[4];
    12. public int usedSize;
    13. public void put(K key,V value){
    14. // 使用hashcode()将key转为数字
    15. int hashKey = key.hashCode();
    16. int index = hashKey % array.length;
    17. Node node = new Node<>(key,value);
    18. Node cur = array[index];
    19. if(cur == null){
    20. array[index] = node;
    21. }else{
    22. while (cur.next != null){
    23. if(cur.key.equals(key)){
    24. cur.value = value;
    25. break;
    26. }
    27. cur = cur.next;
    28. }
    29. cur.next = node;
    30. }
    31. this.usedSize++;
    32. if(loadFactor() >= 0.75){
    33. resize();;
    34. }
    35. }
    36. public double loadFactor(){
    37. return this.usedSize *1.0 /array.length;
    38. }
    39. public void resize(){
    40. Node[] newArray = new Node[array.length*2];
    41. for (int i = 0; i < array.length; i++) {
    42. // 得到i下标的节点
    43. Node cur = array[i];
    44. while (cur!= null){
    45. Node curNext = cur.next;
    46. // 遍历
    47. int index = (cur.key.hashCode())% newArray.length;
    48. Node curNew = newArray[index];
    49. if(curNew == null){
    50. newArray[index] = cur;
    51. }else{
    52. while (curNew.next != null){
    53. curNew = curNew.next;
    54. }
    55. curNew.next = cur;
    56. }
    57. cur.next = null;
    58. cur = curNext;
    59. }
    60. }
    61. }
    62. public V get(K key){
    63. int index = key.hashCode() % array.length;
    64. Node cur = array[index];
    65. while (cur != null){
    66. if(cur.key.equals(key)){
    67. return cur.value;
    68. }
    69. cur = cur.next;
    70. }
    71. return null;
    72. }

    7.3 hashcode和equals的作用

    hashcode:将key转为数字,可以决定该元素存储在数组的哪个位置。

    equals:比较元素的key是否相同。

    • 当hashcode结果一样,equals结果一样吗?

            不一定。hashcode相同,说明在数组中的存储位置相同,不能证明在哈希桶的位置是否相同。

    • 当equals一样,hashcode结果一样吗?

            一样。元素的key一样,肯定存储位置是相同的。所以hashcode一定是一样的。

    八、HashMap源码解读

    8.1 HashMap的默认容量是16

    8.2 HashMap的最大容量:1<<30

    8.3 HashMap的容量必须为2的倍数

    当容量是2的倍数时,key&(table.length-1) 和key%table.length的结果相同。 

    8.4 HashMap的默认负载因子是0.75

    8.5 HashMap有四种构造方法

    1. // 方法1:传入初始大小和负载因子
    2. public HashMap(int initialCapacity, float loadFactor) {
    3. if (initialCapacity < 0)
    4. throw new IllegalArgumentException("Illegal initial capacity: " +
    5. initialCapacity);
    6. if (initialCapacity > MAXIMUM_CAPACITY)
    7. initialCapacity = MAXIMUM_CAPACITY;
    8. if (loadFactor <= 0 || Float.isNaN(loadFactor))
    9. throw new IllegalArgumentException("Illegal load factor: " +
    10. loadFactor);
    11. this.loadFactor = loadFactor;
    12. this.threshold = tableSizeFor(initialCapacity);
    13. }
    14. // 方法2:传入初始大小
    15. public HashMap(int initialCapacity) {
    16. this(initialCapacity, DEFAULT_LOAD_FACTOR);
    17. }
    18. // 方法3:不带任何参数
    19. public HashMap() {
    20. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    21. }
    22. // 方法4:传入一个Map参数
    23. public HashMap(Map m) {
    24. this.loadFactor = DEFAULT_LOAD_FACTOR;
    25. putMapEntries(m, false);
    26. }

    最常用的是第三种方法。

     该方法的初始容量大小是0,第一次put的时候容量大小变成16

    8.6 HashMap中的单链表变红黑树的条件

    当前链表的长度超过8.并且当前桶的个数大于等于64的时候,才会将当前这个超过8的链表变成红黑树,否则只是2倍扩容。

     

    九、常见的考题

    • 如果 new HashMap(19),那么 bucket 数组有多大?

            32。数组大小必须是2的倍数

    • HashMap 什么时候开辟 bucket 数组占用内存?

            第一次put的时候

    • HashMap 何时扩容?

           计算出的负载因子大于等于默认负载因子

    • 当两个对象的 hashCode 相同时会发生什么?

           发生哈希冲突

    • 如果两个键的 hashCode 相同,你如何获取值对象?

           遍历当前的链表,通过equals方法确定和查询key一样的key的value

    • 你了解重新调整 HashMap 大小存在什么问题吗?

           要进行重新哈希

  • 相关阅读:
    Modularized Interaction Network for Named Entity Recognition
    Nature、science、cell旗下刊物
    北邮 数字系统设计 13 Datapath&Control
    Linux下IIC子系统和触摸屏驱动
    新手快速上手IDEA【常用快捷键】
    vite脚手架搭建vitepress路径别名设置
    MyBatis篇---第四篇
    简单实现spring的set依赖注入
    代码随想录算法训练营Day55 | 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结篇 | Python | 个人记录向
    Appium切换Android设备输入法, 以及回车按键操作
  • 原文地址:https://blog.csdn.net/weixin_44258092/article/details/126455459