刷穿劍指offer-Day14-哈希表I 基礎知識整理

刷穿劍指offer-Day14-哈希表I 基礎知識整理

引子

哈希表作為算法解題中的top數據結構,因為其查找、插入、刪除的平均復雜度都是O(1),可以大幅度縮減時間復雜度,所以成為一種空間換時間的解題方式。

有人相愛,有人夜里開車看海,有人leetcode第一題都做不出來。

很多第一次刷力扣算法的朋友,都在 twoSum 這道題中看到了這句評論,也相信有90%的朋友在第一次在做這道題的時候也和我一樣,使用了雙層for循環(huán)的O(n ^ 2)時間復雜度去提交。

直到后來,學習了哈希表才了解了更高效的解題方法。那么今天,我們就要開始哈希表的學習了,它真的是所有數據結構中扛把子級別的選手,需要我們認真去對待。

哈希函數與尋址表

哈希函數是一個可以將任意長度的數據塊映射到固定長度的值,這個步驟稱為hash,也就是散列。

而哈希表又稱為散列表,是一種線性表的存儲結構,它由一個哈希函數和一個直接尋址表組成。

哈希表經常作為面試考點,力扣上就有對應的兩道題目 設計哈希集合 和 設計哈希映射。只有我們理解了哈希表的實現原理才談得上設計,讓我們一步步的去理解哈希表吧。

上面我們提到了哈希表是由一個哈希函數和一個直接尋址表組成,這兩者缺一不可。根據哈希函數的定義,我們可以變相的理解為不管系統(tǒng)告訴我們的是一個數字、字符串還是其他的什么,我都可以將其轉化成一個具體的標識符與和尋址表做對應。

Python有hash函數,而Java中存在hashCode,他們都可以將任意的內容轉化為一個固定的整數。那么,我們將轉化后的整數與尋址表的下標做對應,就完成了一個理論上的hash表。

然而,整數的范圍太大了,我們初始化一個如此大的尋址表,然后只存寥寥幾個數組,會造成多大的空間浪費啊,這顯然是不切實際的想法。那么如何節(jié)約內存開銷?我們在刷穿劍指offer系列Day3中講解了整數的取模與快速冪操作。當時還專門引出了一段前戲,問題是:

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

沒錯,既然我們想縮小尋址表的空間,我們就可以通過取模的操作來實現,然而使用什么數字進行取模才能分布均勻呢,當然是盡可能大的質數。比如最小的7、13 、17 等等...

我們每輸入一個內容,通過哈希函數轉換后取模,就可以定位到有限的尋址表了。但這樣操作,又會引出一個問題。即便我們使用足夠大的質數,難免會出現哈希后取模結果一樣的場景,那該怎么辦?

這種場景叫做 哈希沖突!

哈希沖突

上面提到,由于我們?yōu)榱斯?jié)約空間所以縮小了尋址表的大小,導致可能會出現哈希沖突的情況。

比如,我們創(chuàng)建了一個長度為7的尋址表,此時我們需要保存{x:100}和{y:200},x和y兩個key在執(zhí)行哈希后的結果分別為1和8。那么兩者對7取模的結果都為1。

那么,當遇到這種情況應該如何解決呢?一般常見的有兩種解決辦法:

  1. 開放尋址法(開放地址法),又細分為線性探查、二次探查、二度哈希
  2. 鏈式地址法(拉鏈發(fā))

對于第一種解決辦法,當x存入尋址表index1位置后,y發(fā)現x把index1這個下標占了,它就依次往后找2有沒有占,沒有占我就用index2,如果依然被占了就繼續(xù)往后找,直到找到結果。

很多人會說,這樣錯亂的保存,我們在取數的時候該怎么拿呢?我們來思考下

  • 如果是找x,我們hash后取模找到尋址表的1節(jié)點,此時節(jié)點數據為["x", 100],找到了key,直接返回100。
  • 如果是找y,我們hash后取模找到尋址表的1節(jié)點,發(fā)現不是y,那就往后找,找到下一個節(jié)點["y",200]是結果,返回200

這樣的操作方式就是開放尋址法中的線性探查。至于二次探查和二度哈希,原理也是如此。

對于第二種解決辦法,當x存入尋址表index1位置后,y發(fā)現x把index1這個下標占了,此時我們就需要在index1的位置創(chuàng)建一個鏈表,然后將x、y分別添加至鏈表。這樣下次在查找x和y時,遍歷鏈表節(jié)點并返回即可。

不管是第一種辦法,還是第二種辦法,出現了這種哈希沖突后,都會造成哈希表的時間復雜度提升,所以說到哈希表的時間復雜度都是指的平均時間復雜度。

介紹了這么多內容,不做一道題目,怎么驗證知識的掌握程度,讓我們先來做一道簡單的設計哈希集合題目。

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來完成相關類型題目。

歡迎關注我的公眾號: 清風Python,帶你每日學習Python算法刷題的同時,了解更多python小知識。

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容