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

結合 LeetCode 談談哈希表在算法問題上的應用

LeetCode 前一百道題中總結了些哈希表(unordered_map)應用於算法問題的場景,在恰當的時候使用哈希表能夠大幅提高算法效率,好比:統計字符串中每一個字符或單詞出現的次數、從一維數組中選擇出兩個數使之與某數相等。c++

在開始以前,首先簡要的介紹一下哈希表(又稱散列表),心急的同窗能夠跳轉到LeetCode部分算法

哈希表介紹

哈希表查找的時間複雜度最差是O(n),平均時間複雜度O(1),所以,理想狀態哈希表的使用和數組很像。sql

散列表使用某種算法操做(散列函數)將鍵轉化爲數組的索引來訪問數組中的數據,這樣能夠經過Key-value的方式來訪問數據,達到常數級別的存取效率。如今的nosql數據庫都是採用key-value的方式來訪問存儲數據。數據庫

散列表是算法在時間和空間上作出權衡的經典例子。經過一個散列函數,將鍵值key映射到記錄的訪問地址,達到快速查找的目的。若是沒有內存限制,咱們能夠直接將鍵做爲數組的索引,全部的操做操做只須要一次訪問內存就能夠完成。數組

散列函數

散列函數就是將鍵轉化爲數組索引的過程,這個函數應該易於計算且可以均與分佈全部的鍵。bash

散列函數最經常使用的方法是除留餘數法,一般被除數選用素數,這樣才能保證鍵值的均勻散佈。nosql

散列函數和鍵的類型有關,每種數據類型都須要相應的散列函數;好比鍵的類型是整數,那咱們能夠直接使用除留餘數法;鍵的類型是字符串的時候咱們任然可使用除留餘數法,能夠將字符串當作一個特別大的整數。函數

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值ui

碰撞解決

不一樣的關鍵字獲得同一個散列地址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
相關文章
相關標籤/搜索