真的太重要了,面试出现的概率达到了 99%!!!对于哈希表的知识(建议收藏)

1、哈希表的引入:

前言:

顺序结构 以及 平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经过关键码的多次比较 。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( log2 N),搜索的效率 取决于搜索过程中元素的 比较次数

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

当向该结构中:

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

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

  • 哈希表 : 数组(存储元素) + 哈希函数(根据元素,得到数组中的一个合法下标)

(哈希函数的计算:一般是 该元素 % 数组长度

举个例子:

数据集合: {1,7,6,4,5,9}
在这里插入图片描述
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

2、哈希冲突

(1)概念:

哈希冲突:不同关键字通过相同哈希函数计算出相同的哈希地址

例如上面的那个例子中:

如果集合中多添加了一个元素:14
在这里插入图片描述此时,元素 4 和 14 所计算出来哈希地址是相同的,那数组中该怎么保存这两个元素,这就是哈希冲突

(哈希表底层数组的容量往往是小于实际要存储的关键字的数量的)
哈希冲突的发生是必然的 - 衣柜的容量是有限的,而衣服是无限的,总会出现哈希冲突的
但是我们可以尽量的 降低冲突率

  • 1、重新设计哈希函数
  • 2、理论上,数组长度可以使用素数(数组长度是 2 的 n 次方)
  • 3、设置一个负载因子的阈值,达到阈值时,通过扩容,来降低冲突率

(2)避免哈希冲突负载因子调节

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

(3)哈希冲突的解决方法:(重重重点)

1、探测法(开放寻址法)

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的“下一个” 空位置中去。

那如何寻找下一个空位置呢?
在这里插入图片描述
以这个为例,此时 4 和 14位置冲突

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

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
    在这里插入图片描述
  • 采用这种方式处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,14查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
2、拉链法(哈希桶)(Java 的 HashMap选用这种)(重点学习!!)

使用另一种数据结构存储冲突元素 (单链表 -> 红黑树)

(具有相同地址的元素归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。)

在这里插入图片描述
从上图可以看出,每个桶中放的都是发生哈希冲突的元素。

如果数组中 存储的元素不是 int 类型 怎么办?(Person 对象)

(所有的 Object 类,都有一个方法 int hashCode())

  • 1、 通过该方法,将一个对象变为 int 类型(int hashValue = key.hashCode())
  • 2、把 int 类型转成一个合法下标(int index = hashValue % array.length)

代码实现拉链法:(重点掌握 插入元素的过程!!!)

public class MyHashTable {
    //1、数组:
    private Node[] array = new Node[11];
    //2、维护哈希表中的元素个数
    private int size;
    //true:key 之前不在哈希表中
    //false:key 之前已经在哈希表
    public boolean insert(Integer key) {
        //1、把对象转成 int 类型
        int hashValue = key.hashCode();
        //2、把 hashValue 转成合法下标
        int index = hashValue % array.length;
        //3、遍历 index 位置的链表,确定 key 在不在元素中
        Node cur = array[index];
        while (cur != null) {
            if (key.equals(cur.key)) {
                return false;
            }
            cur = cur.next;
        }
        //4、把 key 装入结点中,对插入到对应的链表中(JDK 1.8 改成尾插了)
        Node node = new Node(key);
        node.next = array[index];
        array[index] = node;
        //5、维护元素个数
        size++;
        return true;
    }

    public boolean remove(Integer key) {
        int hashValue = key.hashCode();
        int index = hashValue % array.length;
        Node cur = array[index];//要删除的当前结点
        //删除第一个结点
        if (key.equals(cur.key)) {
            cur.next = null;
            return true;
        }
        Node prev = null;//要删除的结点的前一个结点
        while (cur != null) {
            if (key.equals(cur.key)) {
                if (cur.next != null) {
                    //删除中间结点
                    prev.next = cur.next;
                } else {
                    //删除最后一个结点
                    prev.next = null;
                }
                size--;
                return true;
            }
            prev = cur;
            cur = cur.next;
        }
        //没找到要删除的元素
        return false;
    }

    public boolean contains(Integer key) {
        //找到对应的下标
        int hashValue = key.hashCode();
        int index = hashValue % array.length;
        Node cur = array[index];
        while (cur != null) {
            if (key.equals(cur.key)) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
}

在这里插入图片描述
HashMap 和 HashSet 中有关对象时,都需要覆写 equels 和 hashCode 方法

如果不重写 hashCode ,就不能保证两个 index 是相同的(默认的 hashCode 十有八九是不相同的)
如果两个对象想通过 hashMap 找到,要意味两者的 hashCode 必须是相等的

3、Java 类集的关系:

在这里插入图片描述

  • 1、HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set 接口
  • 2、 java 中使用的是哈希桶方式解决冲突的
  • 3、java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的
  • 4、java 会在冲突链表长度大于一定阈值(8),数组长度大于64 后,将链表转变为搜索树(红黑树)

为什么 HashMap 链表长度超过8才转为红黑树?

  • (1)为什么要转红黑树:
    • 红黑树查找时间复杂度:O(logn)
    • 链表查找时间复杂度:O(n)
  • (2)为什么一开始不用红黑树:
    • 因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。而是否足够多就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是 UNTREEIFY_THRESHOLD(默认值6)
  • (3)转换阈值 8 是怎么来的?
    • 源码上说,为了配合使用分布良好的hashCode,树节点很少使用。并且在理想状态下,受随机分布的hashCode影响,链表中的节点遵循泊松分布,而且根据统计,链表中节点数是8的概率已经小于千万分之一,而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树,提高性能。
    • 如果数组长度 < 64的话,链表长度 > 8,链表不会变为红黑树,而是让数组扩容
    • 如果数组长度 > 64的话,链表长度 > 8,链表变为红黑树
  • 61
    点赞
  • 312
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值