1. 程式人生 > 程式設計 >搞定技術面試: 結合 LeetCode 談談雜湊表在演演算法問題上的應用

搞定技術面試: 結合 LeetCode 談談雜湊表在演演算法問題上的應用

結合 LeetCode 談談雜湊表在演演算法問題上的應用

LeetCode 前一百道題中總結了些雜湊表(unordered_map)應用於演演算法問題的場景,在恰當的時候使用雜湊表可以大幅提升演演算法效率,比如:統計字串中每個字元或單詞出現的次數、從一維陣列中選擇出兩個數使之與某數相等。

在開始之前,首先簡要的介紹一下雜湊表(又稱散列表),心急的同學可以跳轉到LeetCode部分

雜湊表介紹

雜湊表查詢的時間複雜度最差是O(n),平均時間複雜度O(1),因此,理想狀態雜湊表的使用和陣列很像。

散列表使用某種演演算法操作(雜湊函式)將鍵轉化為陣列的索引來訪問陣列中的資料,這樣可以通過Key-value

的方式來訪問資料,達到常數級別的存取效率。現在的nosql資料庫都是採用key-value的方式來訪問儲存資料。

散列表是演演算法在時間和空間上做出權衡的經典例子。通過一個雜湊函式,將鍵值key對映到記錄的訪問地址,達到快速查詢的目的。如果沒有記憶體限制,我們可以直接將鍵作為陣列的索引,所有的操作操作只需要一次訪問記憶體就可以完成。

雜湊函式

雜湊函式就是將鍵轉化為陣列索引的過程,這個函式應該易於計算且能夠均與分佈所有的鍵。

雜湊函式最常用的方法是除留餘數法,通常被除數選用素數,這樣才能保證鍵值的均勻散佈。

雜湊函式和鍵的型別有關,每種資料型別都需要相應的雜湊函式;比如鍵的型別是整數,那我們可以直接使用除留餘數法

;鍵的型別是字串的時候我們任然可以使用除留餘數法,可以將字串當做一個特別大的整數。

int hash = 0;
for (int i=0;i<s.length();i++){
	hash = (R*hash +s.charAt(i)%M);
}
複製程式碼

或者

Hash hashCode(char *key){
	int offset = 5;
	Hash hashCode = 0;
	while(*key){
		hashCode = (hashCode << offset) + *key++;
	}
	return hashCode;		
}
複製程式碼

使用時 hashCode(key) & (size-1)

就可以得到一個 size-1 範圍內的hash值

碰撞解決

不同的關鍵字得到同一個雜湊地址f(key1)=f(key2),即為碰撞 。這是我們需要儘量避免的情況。常見的處理方法有:

  1. 拉鍊法
  2. 開放地址法

拉鍊法

將大小為M的陣列中的每個元素指向一條連結串列,連結串列中的每個節點都儲存了雜湊值為該元素索引的鍵值對。每條連結串列的平均長度是N/M,N是鍵值對的總個數。如下圖:

新增操作:

  1. 通過hash函式得到hashCode
  2. 通過hashcode得到index
  3. 如果index處沒有連結串列,建立好新結點,作為新連結串列的首結點
  4. 如果index處已經有連結串列,先要遍歷看key是否已經存在,如果存在直接返回,如果不存在,加入連結串列頭部

刪除操作:

  1. 通過hash函式得到hashCode
  2. 通過hashcode得到index
  3. 遍歷連結串列,刪除結點

開放地址法

使用大小為M的陣列儲存N個鍵值對,當碰撞發生時,直接檢查散列表中的下一個位置。 檢查的方法可以是線性檢測、平方檢測、雙雜湊等方法,主要區別在於檢測的下一個位置。 《演演算法導論》中更推薦雙雜湊方法。

// 插入演演算法
HASH-INSERT(T,k)
    i = 0
    repeat
        j = h(k,i)
        if T[j] == NIL
            T[j] = k
            return j
        else
            i++
    until i == m
    error "hash table overflow"

// 搜尋演演算法
HASH-SEARCH(T,k)
    i = 0
    repeat
        j = j(k,i)
        if T[j] == k
            return j
        i++
    until T[j] == NIL or i == m
    return NIL
複製程式碼

LeetCode 中雜湊表的題目

在練習 LeetCode 的過程中,我數次碰到了可以使用 表來簡化問題的場景。

由元素值去尋找索引的問題

給一個陣列nums = [2,7,11,15],要求求得兩個數使他們的和為 target = 9

這個簡單的問題,如果使用迴圈暴力求解的話,也可以很快獲得解,但是時間複雜度是O(n^2),如果這個陣列很長的話,有百萬千萬數量級,就需要超級多的時間才可以迴圈完。

一個快速的解法是,使用表先記下每個數值的索引,接著迴圈一次,判斷target-nums[i],在不在表中,就可以快速找到一組解。此處,key是數值,value是數值對應的索引,是利用數值快速尋找索引的方法。

vector<int> twoSum(vector<int>& nums,int target) 
{
        unordered_map<int,int> maps;
        int size = nums.size();
        for(int i = 0; i < size; ++i)
            maps[nums[i]] = i;

        for (int i = 0; i < size; ++i) {
            int left = target - nums[i];
            if(maps.count(left) > 0 && maps[left] != i)
            {
                return vector<int>({i,maps[left]});
            }
        }
        return vector<int>();
    }
複製程式碼

記下某個元素出現次數的問題

數獨合法性判斷問題包含所有字元的最短子串問題都是利用雜湊表來計算某個元素出現次數的問題。

在這種所有元素的種類有限的問題中,我們還可以使用陣列vector<int>來代替雜湊表unordered_map<int,int>、unordered_map<char,int>、進行統計,因為1-9總共有9種可能,ASCII碼元素總共有128種可能性,這實際上便是雜湊函式為f(int x){return x;}的特殊情況。

數獨問題

數獨問題要求每行、每列、以及整個表格分為9個子塊,每個子塊內,1-9只能出現一次,不能重複。我們可以為每行每列建立雜湊表,總共 27個表,將元素加入表中,一旦出現有表中的某個元素大於1,便可判斷數獨不合格。

bool isValidSudoku(vector<vector<char>>& board)
    {
        vector<unordered_map<char,int>> rows(9);
        vector<unordered_map<char,int>> cols(9);
        vector<unordered_map<char,int>> subs(9);
        for (int i=0; i<9; i++)
        {
            for (int j=0; j<9; j++)
            {
                // row
                char ch = board[i][j];
                if(ch != '.')
                {
                    rows[i][ch]++;
                    if(rows[i][ch] > 1)
                        return false;

                    cols[j][ch]++;
                    if(cols[j][ch] > 1)
                        return false;

                    int idx = i/3 + j-(j%3);
                    subs[idx][ch]++;
                    if(subs[idx][ch] > 1)
                        return false;
                }
            }
        }
        return true;
    }
複製程式碼

同樣,在求解數獨問題時,我們可以利用動態規劃方法,在插入每個元素前,利用雜湊表檢查插入的元素是否合法,如果不合法,則恢復"作案現場",返回到上一層。

將本不是相同 key 的資料轉換為相同 key

有的問題,從列表中找到所有是相同元素,不同排列組成的問題,比如Group Anagrams問題。

Input: ["eat","tea","tan","ate","nat","bat"],Output:
[
  ["ate","eat","tea"],["nat","tan"],["bat"]
]
複製程式碼

我們可以利用雜湊表的原理,自己結合排列組合的特性,將所有元素按字典序轉換出來,便可以使這些元素的結果相同,指向同一個索引。即自己寫了個雜湊函式f(string){return sort(string);}。問題的解法如下:

vector<vector<string>> groupAnagrams(vector<string>& strs)
    {
        vector<vector<string>> result;
        unordered_map<string,vector<string>> map;
        for(int i = 0; i < strs.size(); i++)
        {
            string s = strs[i];
            sort(s.begin(),s.end());
            map[s].push_back(strs[i]);
        }
        for(pair<string,vector<string>> pa:map)
        {
            result.push_back(pa.second);
        }
        return result;
    }
複製程式碼

總結

結合上面這些 LeetCode 上與雜湊表想關的問題,我們可以總結一些雜湊表在演演算法問題中改善演演算法效率的思路。

  1. 在那些需要一次一次遍歷,去尋找元素的問題中,可以將問題轉化為根據元素的內容去尋找索引,雜湊表在這方面的時間效率是賊高的;
  2. 在一些字串詞頻統計問題、數獨問題等問題中,可以利用雜湊函式來計算某個元素出現的次數,作為演演算法的輔助工具;
  3. 還有些問題,可以利用雜湊函式的思路,讓幾個不同的元素獲得同樣的結果,從而實現一個聚類。

references

  1. Algotithm 3rd
  2. LeetCode hash table