刷穿剑指offer-Day14-哈希表I 基础知识整理

简介: 刷穿剑指offer-Day14-哈希表I 基础知识整理

刷穿剑指offer-Day14-哈希表I 基础知识整理



引子

哈希表作为算法解题中的top数据结构,因为其查找、插入、删除的平均复杂度都是O(1),可以大幅度缩减时间复杂度,所以成为一种空间换时间的解题方式。

有人相爱,有人夜里开车看海,有人leetcode第一题都做不出来。

很多第一次刷力扣算法的朋友,都在 twoSum 这道题中看到了这句评论,也相信有90%的朋友在第一次在做这道题的时候也和我一样,使用了双层for循环的O(n ^ 2)时间复杂度去提交。

网络异常,图片无法展示
|

直到后来,学习了哈希表才了解了更高效的解题方法。那么今天,我们就要开始哈希表的学习了,它真的是所有数据结构中扛把子级别的选手,需要我们认真去对待。


哈希函数与寻址表

哈希函数是一个可以将任意长度的数据块映射到固定长度的值,这个步骤称为hash,也就是散列。

而哈希表又称为散列表,是一种线性表的存储结构,它由一个哈希函数和一个直接寻址表组成。

哈希表经常作为面试考点,力扣上就有对应的两道题目 设计哈希集合 和 设计哈希映射。只有我们理解了哈希表的实现原理才谈得上设计,让我们一步步的去理解哈希表吧。

上面我们提到了哈希表是由一个哈希函数和一个直接寻址表组成,这两者缺一不可。根据哈希函数的定义,我们可以变相的理解为不管系统告诉我们的是一个数字、字符串还是其他的什么,我都可以将其转化成一个具体的标识符与和寻址表做对应。

网络异常,图片无法展示
|

Python有hash函数,而Java中存在hashCode,他们都可以将任意的内容转化为一个固定的整数。那么,我们将转化后的整数与寻址表的下标做对应,就完成了一个理论上的hash表。

然而,整数的范围太大了,我们初始化一个如此大的寻址表,然后只存寥寥几个数组,会造成多大的空间浪费啊,这显然是不切实际的想法。那么如何节约内存开销?我们在刷穿剑指offer系列Day3中讲解了整数的取模与快速幂操作。当时还专门引出了一段前戏,问题是:

10 ^ 9 + 7取余,到底有什么含义?

没错,既然我们想缩小寻址表的空间,我们就可以通过取模的操作来实现,然而使用什么数字进行取模才能分布均匀呢,当然是尽可能大的质数。比如最小的7、13 、17 等等...

我们每输入一个内容,通过哈希函数转换后取模,就可以定位到有限的寻址表了。但这样操作,又会引出一个问题。即便我们使用足够大的质数,难免会出现哈希后取模结果一样的场景,那该怎么办?

这种场景叫做 哈希冲突


哈希冲突

上面提到,由于我们为了节约空间所以缩小了寻址表的大小,导致可能会出现哈希冲突的情况。

比如,我们创建了一个长度为7的寻址表,此时我们需要保存{x:100}和{y:200},x和y两个key在执行哈希后的结果分别为1和8。那么两者对7取模的结果都为1。

那么,当遇到这种情况应该如何解决呢?一般常见的有两种解决办法:

  1. 开放寻址法(开放地址法),又细分为线性探查、二次探查、二度哈希
  2. 链式地址法(拉链发)

对于第一种解决办法,当x存入寻址表index1位置后,y发现x把index1这个下标占了,它就依次往后找2有没有占,没有占我就用index2,如果依然被占了就继续往后找,直到找到结果。

网络异常,图片无法展示
|

很多人会说,这样错乱的保存,我们在取数的时候该怎么拿呢?我们来思考下

  • 如果是找x,我们hash后取模找到寻址表的1节点,此时节点数据为["x", 100],找到了key,直接返回100。
  • 如果是找y,我们hash后取模找到寻址表的1节点,发现不是y,那就往后找,找到下一个节点["y",200]是结果,返回200

这样的操作方式就是开放寻址法中的线性探查。至于二次探查和二度哈希,原理也是如此。

对于第二种解决办法,当x存入寻址表index1位置后,y发现x把index1这个下标占了,此时我们就需要在index1的位置创建一个链表,然后将x、y分别添加至链表。这样下次在查找x和y时,遍历链表节点并返回即可。

网络异常,图片无法展示
|

不管是第一种办法,还是第二种办法,出现了这种哈希冲突后,都会造成哈希表的时间复杂度提升,所以说到哈希表的时间复杂度都是指的平均时间复杂度

介绍了这么多内容,不做一道题目,怎么验证知识的掌握程度,让我们先来做一道简单的设计哈希集合题目。


705.设计哈希集合


https://leetcode-cn.com/problems/design-hashset/solution/705-she-ji-ha-xi-ji-he-xiang-na-yao-duo-2jo2w/

难度:简单


题目

不使用任何内建的哈希表库设计一个哈希集合(HashSet)。实现 MyHashSet 类:

  • void add(key) 向哈希集合中插入值 key 。
  • bool contains(key) 返回哈希集合中是否存在这个值 key 。
  • void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。

提示:

  • 0 <= key <= 10 ^ 6
  • 最多调用 10 ^ 4 次 add、remove 和 contains 。


示例

输入:
["MyHashSet", "add", "add", "contains", "contains", "add", "contains", "remove", "contains"]
[[], [1], [2], [1], [3], [2], [2], [2], [2]]
输出:
[null, null, null, true, false, null, true, null, false]
解释:
MyHashSet myHashSet = new MyHashSet();
myHashSet.add(1);      // set = [1]
myHashSet.add(2);      // set = [1, 2]
myHashSet.contains(1); // 返回 True
myHashSet.contains(3); // 返回 False ,(未找到)
myHashSet.add(2);      // set = [1, 2]
myHashSet.contains(2); // 返回 True
myHashSet.remove(2);   // set = [1]
myHashSet.contains(2); // 返回 False ,(已移除)


分析

通过我们介绍的哈希表知识,再来做这道题就不会显得无从下手了。

首先,我们可以考虑一种极限场景,即构造一个 0 <= key <= 10 ^ 6 长度的寻址表。

这样,无需任何操作,就可以在O(1)的时间内返回答案了。

验证是可行的。但这样的解题很明显面试的时候要被吊打。

所以,我们还是来考虑链式地址法,即通过数组与链表的方式设计一个哈希集合。

两种解题如下:


与key等长的寻址表


Python:

class MyHashSet:
    def __init__(self):
        self.my_set = [False] * 1000001
    def add(self, key: int) -> None:
        self.my_set[key] = True
    def remove(self, key: int) -> None:
        self.my_set[key] = False
    def contains(self, key: int) -> bool:
        return self.my_set[key]


Java:

class MyHashSet {
    boolean[] mySet = new boolean[1000001];
    public void add(int key) {
        mySet[key] = true;
    }
    public void remove(int key) {
        mySet[key] = false;
    }
    public boolean contains(int key) {
        return mySet[key];
    }
}


链式地址法解题


Python:

class MyHashSet:
    def __init__(self):
        self.mod = 1007
        self.table = [[] for _ in range(self.mod)]
    def hash(self, key):
        return key % self.mod
    def div(self, key):
        return key // self.mod
    def add(self, key):
        hash_key = self.hash(key)
        if not self.table[hash_key]:
            self.table[hash_key] = [0] * self.mod
        self.table[hash_key][self.div(key)] = 1
    def remove(self, key):
        hash_key = self.hash(key)
        if self.table[hash_key]:
            self.table[hash_key][self.div(key)] = 0
    def contains(self, key):
        hash_key = self.hash(key)
        return self.table[hash_key] != [] and self.table[hash_key][self.div(key)] == 1


Java:

class MyHashSet {
    private static final int BASE = 1007;
    private LinkedList[] mySet;
    private static int hash(int key) {
        return key % BASE;
    }
    public MyHashSet() {
        mySet = new LinkedList[BASE];
        for (int i = 0; i < BASE; i++) {
            mySet[i] = new LinkedList<Integer>();
        }
    }
    public void add(int key) {
        int h = hash(key);
        Iterator<Integer> iterator = mySet[h].iterator();
        while (iterator.hasNext()) {
            Integer value = iterator.next();
            if (value == key) {
                return;
            }
        }
        mySet[h].offerLast(key);
    }
    public void remove(int key) {
        int h = hash(key);
        Iterator<Integer> iterator = mySet[h].iterator();
        while (iterator.hasNext()) {
            Integer value = iterator.next();
            if (value == key) {
                mySet[h].remove(value);
                return;
            }
        }
    }
    public boolean contains(int key) {
        int h = hash(key);
        Iterator<Integer> iterator = mySet[h].iterator();
        while (iterator.hasNext()) {
            Integer value = iterator.next();
            if (value == key) {
                return true;
            }
        }
        return false;
    }
}

今天关于哈希表的知识就学习到这里,明天我们需要通过学习HashSet和HashMap来完成相关类型题目。




相关文章
|
8月前
代码随想录训练营 Day07 - 哈希表(下)
代码随想录训练营 Day07 - 哈希表(下)
32 0
|
4月前
|
存储 机器学习/深度学习 算法
六六力扣刷题哈希表之三数之和
六六力扣刷题哈希表之三数之和
34 0
|
8月前
|
存储
代码随想录训练营 Day06 - 哈希表(上)
代码随想录训练营 Day06 - 哈希表(上)
32 0
|
9月前
|
算法 程序员
刷爆LeetCode!字节技术官亲码算法面试进阶神技太香了
正赶上金三银四,说到数据结构与算法这个词,肯定有不少人会眉头一皱。也不知从什么时候开始,以字节为主的一大波公司面试开始了对算法的连环拷问。如果事前没有系统地刷一波题的话,算法这一关还是比较难过
|
10月前
|
C++
C/C++ leetcode刷题的各种小tips记录
C/C++ leetcode刷题的各种小tips记录
113 0
|
存储 算法
数据结构刷题:第七天
具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
68 0
数据结构刷题:第七天
刷leetcode-整理字符串 | 刷题打卡
刷leetcode-整理字符串 | 刷题打卡
50 0
|
存储 算法
代码随想录算法训练营第七天 | 哈希表
代码随想录算法训练营第七天 | 哈希表
105 0
|
存储 算法 索引
代码随想录算法训练营第六天 | 哈希表 四道简单题
代码随想录算法训练营第六天 | 哈希表 四道简单题
74 0
|
算法
[每日一题] 4月刷爆数据结构第十二天
大家好,我是泡泡,给大家带来每日一题的目的是为了更好的练习算法,我们的每日一题这个月进度是数据结构,让大家练到各种各样的数据结构题目,熟悉数据结构的增删改查,一年以后,蜕变成为一个不一样的自己!
97 0