ACM金牌選手講解LeetCode演算法《雜湊》

公眾號【程式設計熊】發表於2021-08-07

大家好,我是程式設計熊。

往期文章介紹了《線性表》中的陣列、連結串列、棧、佇列,以及單調棧和滑動視窗。

本期我們學習雜湊,其主要作用是加速我們查詢資料的速度。

文章將從以下幾個方面展開,內容通俗易懂。

若不想了解雜湊原理,直接使用雜湊表刷題的話,可以直接下拉到"常見的雜湊結構"部分。

雜湊

雜湊概述

雜湊表又稱雜湊表,表現形式為將任意長度的輸入,通過雜湊演算法變成固定長度的輸出,雜湊表是一種使用空間換取時間的資料結構。

通常是儲存 <key,value>鍵值對,假設沒有雜湊表,將 <key,value>鍵值對儲存在陣列中,給定key查詢的對應的value的時間複雜度為O(n)

陣列就是常見的雜湊表,下標就是key,對應儲存的值就是value

通過引如雜湊表,將任意長度的輸入key轉化為雜湊表中的下標,將<key,value>鍵值對對映到雜湊表中,進而加速給定給定key,查詢value的速度,時間複雜度降低到O(1)

下圖 以兩個鍵值對<key1,value1><key2,value2>為例,演示了雜湊函式和雜湊表之間的關係,以及在雜湊中起到的作用。

雜湊

雜湊表基本操作

插入

將鍵值對<key,value>插入到雜湊表中。

更新

若雜湊表中已存在鍵值為key的鍵值對,更新雜湊表鍵值對<key,value>

刪除

將鍵值對<key,value>從雜湊表中刪除。

查詢

給定key,有兩種查詢方式。

  1. 查詢key 是否存在於雜湊表中。
  2. 查詢key對應的value

雜湊函式

雜湊函式又稱雜湊函式,即將給定的任意長度的輸入值轉化為陣列的索引(下標)。

如果有一個長度為n的陣列,其可以儲存n對鍵值對,對應的下標為[0,n-1],通常陣列的長度是大於等於鍵值對的數量。

因此我們需要一個雜湊函式,將任意長度的輸入對映到[0,n-1],並且每個不同的key對應的陣列下標一定是不一樣的,即每個陣列下標唯一對應一個key

下圖以三對<key,value>為例,演示了雜湊函式hash將原始key,對映到陣列下標的過程,具體雜湊函式實現可以有很多方法,感興趣的讀者可以自行探究。

雜湊衝突

雜湊衝突的出現源於雜湊函式對兩個不同的鍵key1key2 (key1≠key2),但經過雜湊函式,hash(key1)=hash(key2),將兩個不同的key,對映到了同一個陣列下標位置,導致了雜湊衝突。

下圖以key1="abc"key2="bcd",兩個不同的key,經過雜湊函式,對映到同一個陣列下標X

雜湊衝突

解決雜湊衝突的方法

拉鍊法

hash值相同的key放到一個連結串列中,查詢時從前往後遍歷連結串列,找到想要查詢的key即可。

設需要插入雜湊表的陣列a長度為n,雜湊表陣列長度為m,則拉鍊法查詢任意一個key的期望時間複雜度為O(1+n/m)

下圖展示了需要插入雜湊表的陣列a,雜湊函式h(x),使用拉鍊法解決雜湊衝突的例子。

拉鍊法

開放地址法

從發生衝突的位置起,按照某種規則找到雜湊表中其他空閒的位置,將衝突的元素放入這個空閒的位置。

可以找到空閒位置的條件是: 雜湊表的長度一定要大於存放元素的個數。

發生衝突後,以什麼樣的”規則“找到空閒的位置,有很多種方法:

  • 線行探查法: 從衝突的位置開始,依次判斷下一個位置是否空閒,直至找到空閒位置。
  • 平方探查法: 從衝突的位置x開始,第一次增加1^2個位置,第二次增加2^2...,直至找到空閒的位置。
  • 雙雜湊函式探查法等等

再雜湊法

構造多個雜湊函式,發生衝突時,更換雜湊函式,直至找到空閒位置。

建立公共溢位區

建立公共溢位區,在雜湊表中發生雜湊衝突時,將資料儲存到公共溢位區。

常見的雜湊結構

當解決問題需要快速查詢一個元素/鍵值對,就可以考慮利用雜湊表加速查詢的速度。

C++中常用的雜湊結構有以下三個:

  • 陣列
  • unordered_set(集合)
  • unordered_map(對映: 鍵值對)
種類 底層實現 Key是否有序 Key是否可以重複 Key是否可以修改 增刪查效率
std::unordered_set(集合) 雜湊表 Key無序 Key不可重複 Key不可修改 O(1)
std::unordered_map(對映: 鍵值對) 雜湊表 Key無序 Key不可重複 Key不可修改 O(1)

C++標準庫中的set、map底層基於紅黑樹,將會在後續章節中詳細介紹。

std::unordered_set用法

下面介紹常見的用法,一般可以滿足刷題需要,詳細見https://zh.cppreference.com/w/cpp/container/unordered_set

// 定義一個std::unordered_set
std::unordered_set q;

// 迭代器
// begin: 返回指向起始的迭代器
auto iter = q.begin();
// end: 返回指向末尾的迭代器
auto iter = q.end();

// 容量
// empty: 檢查容器是否為空
bool is_empty =  q.empty();
// size: 返回容納的元素數量
int s = q.size();

// 修改器
// clear: 清除內容
q.clear();
// insert: 插入元素或結點
q.insert(key);
// erase: 擦除元素
q.erase(key);

// 查詢
// count: 返回匹配特定鍵的元素數量
int num = q.count(key);
// find: 尋找帶有特定鍵的元素
auto iter = q.find(key);
// contains: 檢查容器是否含有帶特定鍵的元素
bool is_contains = q.contains(key);

std::unordered_map用法

下面介紹常見的用法,一般可以滿足刷題需要,詳細見https://zh.cppreference.com/w/cpp/container/unordered_map

// 定義一個std::unordered_map
std::unordered_map q;

// 迭代器
// begin: 返回指向起始的迭代器
auto iter = q.begin();
// end: 返回指向末尾的迭代器
auto iter = q.end();

// 容量
// empty: 檢查容器是否為空
bool is_empty =  q.empty();
// size: 返回容納的元素數量
int s = q.size();

// 修改器
// clear: 清除內容
q.clear();
// insert: 插入元素或結點
q.insert(key);
// erase: 擦除元素
q.erase(key);

// 查詢
// count: 返回匹配特定鍵的元素數量
int num = q.count(key);
// find: 尋找帶有特定鍵的元素
auto iter = q.find(key);
// contains: 檢查容器是否含有帶特定鍵的元素
bool is_contains = q.contains(key);

例題

LeetCode 1. 兩數之和

題意

給定一個整數陣列 nums 和一個整數目標值 target,請你在該陣列中找出 和為目標值 target 的那 兩個 整數,並返回它們的陣列下標。

示例

輸入:nums = [2,7,11,15], target = 9
輸出:[0,1]
解釋:因為 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

下圖以示例演示一下雜湊表,將陣列插入到雜湊表中,查詢給定的key,即可以在O(1) 的時間複雜度查詢到,圖中a,b,c,d指代雜湊表的下標。

示例

題解

建立雜湊表,key等於陣列的值,value等於值所對應的下標。

然後遍歷陣列,每次遍歷到位置i時,檢查 target-num[i] 是否存在,注意target-num[i]的位置不能等於i

程式碼

class Solution {
    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> numExist = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; ++i) {
            if (numExist.containsKey(target - nums[i])) {
                return new int[]{i, numExist.get(target - nums[i])};
            }
            numExist.put(nums[i], i);
        }
        return new int[2];
    }
}

LeetCode 128. 最長連續序列

題意

給定一個未排序的整數陣列 nums ,找出數字連續的最長序列(不要求序列元素在原陣列中連續)的長度。

示例

輸入:nums = [100,4,200,1,3,2]
輸出:4
解釋:最長數字連續序列是 [1, 2, 3, 4]。它的長度為 4。

題解

方法一

對陣列數字排序,然後遍歷排序後的陣列,找到最長的連續序列。

時間複雜度O(nlogn)

方法二

雜湊可以快速查詢一個數字。

將陣列數字插入到雜湊表,每次隨便拿出一個,刪除其連續的數字,直至找不到連續的,記錄刪除的長度,可以找到最長連續序列。

下圖以示例展示,如何利用雜湊表,找到最長連續序列。

示例

程式碼

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> q;
        for (int i = 0; i < nums.size(); i++) {
            q.insert(nums[i]);
        }
        int ans = 0;
        while (!q.empty()) {
            int now = *q.begin();
            q.erase(now);
            int l = now - 1, r = now + 1;
            while (q.find(l) != q.end()) {
                q.erase(l);
                l--;
            }
            while(q.find(r) != q.end()) {
                q.erase(r);
                r++;
            }
            l = l + 1, r = r - 1;
            ans = max(ans, r - l + 1);
        }
        return ans;
    }
};

習題推薦

  1. LeetCode 217. 存在重複元素
  2. LeetCode 594. 最長和諧子序列
  3. LeetCode 149. 直線上最多的點數
  4. LeetCode 332. 重新安排行程

【下面是粉絲福利】

【計算機學習核心資源】: 涵蓋了所有計算機學習核心資源,多看看進大廠問題不大。

【github寶藏倉庫】: 對學習和麵試都非常有幫助,學完超過99%同齡人。

如果對你有所幫助,歡迎點贊支援~

相關文章