刷穿剑指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();
        }
    }

    public void add(int key) {
        int h = hash(key);
        Iterator 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 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 iterator = mySet[h].iterator();
        while (iterator.hasNext()) {
            Integer value = iterator.next();
            if (value == key) {
                return true;
            }
        }
        return false;
    }
}

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

欢迎关注我的公众号: 清风Python,带你每日学习Python算法刷题的同时,了解更多python小知识。

我的个人博客:https://qingfengpython.cn

力扣解题合集:https://github.com/BreezePython/AlgorithmMarkdown

你可能感兴趣的:(刷穿剑指offer-Day14-哈希表I 基础知识整理)