幾道和雜湊(雜湊)表有關的面試題

程式設計師吳師兄發表於2019-03-15

幾道和雜湊(雜湊)表有關的面試題

雜湊表概念

雜湊表(Hash table,也叫雜湊表),是根據鍵(Key)而直接訪問在記憶體儲存位置的資料結構。也就是說,它通過計算一個關於鍵值的函式,將所需查詢的資料對映到表中一個位置來訪問記錄,這加快了查詢速度。這個對映函式稱做雜湊函式,存放記錄的陣列稱做雜湊表。

更加詳細的介紹請戳這:

1. 兩數之和

題目來源於 LeetCode 上第 1 號問題: Two Sum。

題目描述

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

你可以假設每種輸入只會對應一個答案。但是,你不能重複利用這個陣列中同樣的元素。

示例:

給定 nums = [2, 7, 11, 15], target = 9

因為 nums[0] + nums[1] = 2 + 7 = 9

所以返回 [0, 1]
複製程式碼

題目解析

使用雜湊表來解決該問題。

首先設定一個 map 容器 record 用來記錄元素的值與索引,然後遍歷陣列 nums 。

  • 每次遍歷時使用臨時變數 complement 用來儲存目標值與當前值的差值
  • 在此次遍歷中查詢 record ,檢視是否有與 complement 一致的值,如果查詢成功則返回查詢值的索引值與當前變數的值i
  • 如果未找到,則在 record 儲存該元素與索引值 i

動畫描述

兩數之和

程式碼實現

// 1. Two Sum
// 時間複雜度:O(n)
// 空間複雜度:O(n)
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> record;
        for(int i = 0 ; i < nums.size() ; i ++){
            int complement = target - nums[i];
            if(record.find(complement) != record.end()){
                int res[] = {i, record[complement]};
                return vector<int>(res, res + 2);
            }
            record[nums[i]] = i;
        }
    }
};
複製程式碼

2. 無重複字元的最長子串

題目來源於 LeetCode 上第 3 號問題: Longest Substring Without Repeating Characters 。

題目描述

給定一個字串,請你找出其中不含有重複字元的 最長子串 的長度。

題目解析

建立一個 HashMap ,建立每個字元和其最後出現位置之間的對映,然後再定義兩個變數 res 和 left ,其中 res 用來記錄最長無重複子串的長度,left 指向該無重複子串左邊的起始位置的前一個,一開始由於是前一個,所以在初始化時就是 -1。

接下來遍歷整個字串,對於每一個遍歷到的字元,如果該字元已經在 HashMap 中存在了,並且如果其對映值大於 left 的話,那麼更新 left 為當前對映值,然後對映值更新為當前座標i,這樣保證了left始終為當前邊界的前一個位置,然後計算視窗長度的時候,直接用 i-left 即可,用來更新結果 res 。

程式碼實現

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int res = 0, left = -1, n = s.size();
        unordered_map<int, int> m;
        for (int i = 0; i < n; ++i) {
            if (m.count(s[i]) && m[s[i]] > left) {
                left = m[s[i]];  
            }
            m[s[i]] = i;
            res = max(res, i - left);            
        }
        return res;
    }
};
複製程式碼

擴充

此題也可以使用滑動視窗的概念來處理。

建立一個 256 位大小的整型陣列 freg ,用來建立字元和其出現位置之間的對映。

維護一個滑動視窗,視窗內的都是沒有重複的字元,去儘可能的擴大視窗的大小,視窗不停的向右滑動。

  • (1)如果當前遍歷到的字元從未出現過,那麼直接擴大右邊界;
  • (2)如果當前遍歷到的字元出現過,則縮小視窗(左邊索引向右移動),然後繼續觀察當前遍歷到的字元;
  • (3)重複(1)(2),直到左邊索引無法再移動;
  • (4)維護一個結果 res,每次用出現過的視窗大小來更新結果 res ,最後返回 res 獲取結果。

動畫描述

無重複字元的最長子串

程式碼實現

// 3. Longest Substring Without Repeating Characters
// 滑動視窗
// 時間複雜度: O(len(s))
// 空間複雜度: O(len(charset))
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int freq[256] = {0};
        int l = 0, r = -1; //滑動視窗為s[l...r]
        int res = 0;
        // 整個迴圈從 l == 0; r == -1 這個空視窗開始
        // 到l == s.size(); r == s.size()-1 這個空視窗截止
        // 在每次迴圈裡逐漸改變視窗, 維護freq, 並記錄當前視窗中是否找到了一個新的最優值
        while(l < s.size()){
            if(r + 1 < s.size() && freq[s[r+1]] == 0){
                r++;
                freq[s[r]]++;
            }else {   //r已經到頭 || freq[s[r+1]] == 1
                freq[s[l]]--;
                l++;
            }
            res = max(res, r-l+1);
        }
        return res;
    }
};
複製程式碼

3. 三數之和

題目來源於 LeetCode 上第 15 號問題: 3Sum 。

題目描述

給定一個包含 n 個整數的陣列 nums,判斷 nums 中是否存在三個元素 *a,b,c ,*使得 a + b + c = 0 ?找出所有滿足條件且不重複的三元組。

題目解析

題目需要我們找出三個數且和為 0 ,那麼除了三個數全是 0 的情況之外,肯定會有負數和正數,所以一開始可以先選擇一個數,然後再去找另外兩個數,這樣只要找到兩個數且和為第一個選擇的數的相反數就行了。也就是說需要列舉 a 和 b ,將 c 的存入 map 即可。

需要注意的是返回的結果中,不能有有重複的結果。這樣的程式碼時間複雜度是 O(n^2)。在這裡可以先將原陣列進行排序,然後再遍歷排序後的陣列,這樣就可以使用雙指標以線性時間複雜度來遍歷所有滿足題意的兩個陣列合。

程式碼實現

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        if (nums.empty() || nums.back() < 0 || nums.front() > 0) return {};
        for (int k = 0; k < nums.size(); ++k) {
            if (nums[k] > 0) break;
            if (k > 0 && nums[k] == nums[k - 1]) continue;
            int target = 0 - nums[k];
            int i = k + 1, j = nums.size() - 1;
            while (i < j) {
                if (nums[i] + nums[j] == target) {
                    res.push_back({nums[k], nums[i], nums[j]});
                    while (i < j && nums[i] == nums[i + 1]) ++i;
                    while (i < j && nums[j] == nums[j - 1]) --j;
                    ++i; --j;
                } else if (nums[i] + nums[j] < target) ++i;
                else --j;
            }
        }
        return res;
    }
};
複製程式碼

4. 重複的 DNA 序列

題目來源於 LeetCode 上第 187 號問題: Repeated DNA Sequences 。

題目描述

所有 DNA 由一系列縮寫為 A,C,G 和 T 的核苷酸組成,例如:“ACGAATTCCG”。在研究 DNA 時,識別 DNA 中的重複序列有時會對研究非常有幫助。

編寫一個函式來查詢 DNA 分子中所有出現超過一次的 10 個字母長的序列(子串)。

題目解析

首先,先將 A , C , G , T 的 ASCII 碼用二進位制來表示:

A: 0100 0001  C: 0100 0011  G: 0100 0111  T: 0101 0100

通過觀察發現每個字元的後三位都不相同,因此可以用末尾的三位來區分這四個字元。

題目要求是查詢 10 個字母長的序列,這裡我們將每個字元用三位來區分的話,10 個字元就需要 30 位 ,在32位機上也 OK 。

為了提取出後 30 位,需要使用 mask ,取值為 0x7ffffff(二進位制表示含有 27 個 1) ,先用此 mask 可取出整個序列的後 27 位,然後再向左平移三位可取出 10 個字母長的序列 ( 30 位)。

為了儲存子串的頻率,這裡使用雜湊表

首先當取出第十個字元時,將其存在雜湊表裡,和該字串出現頻率對映,之後每向左移三位替換一個字元,查詢新字串在雜湊表裡出現次數,如果之前剛好出現過一次,則將當前字串存入返回值的陣列並將其出現次數加一,如果從未出現過,則將其對映到 1。

解題程式碼

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> res;
        if (s.size() <= 10) return res;
        int mask = 0x7ffffff, cur = 0;
        unordered_map<int, int> m;
        for (int i = 0; i < 9; ++i) {
            cur = (cur << 3) | (s[i] & 7);
        }
        for (int i = 9; i < s.size(); ++i) {
            cur = ((cur & mask) << 3) | (s[i] & 7);
            if (m.count(cur)) {
                if (m[cur] == 1) res.push_back(s.substr(i - 9, 10));
                ++m[cur]; 
            } else {
                m[cur] = 1;
            }
        }
        return res;
    }
};
複製程式碼

5. 兩個陣列的交集

題目來源於 LeetCode 上第 349 號問題: Intersection of Two Arrays。

題目描述

給定兩個陣列,編寫一個函式來計算它們的交集。

題目解析

容器類 set 的使用。

  • 遍歷 num1,通過 set 容器 record 儲存 num1 的元素
  • 遍歷 num2,在 record 中查詢是否有相同的元素,如果有,用 set 容器 resultSet 進行儲存
  • 將 resultSet 轉換為 vector 型別

動畫描述

兩個陣列的交集

程式碼實現

// 時間複雜度: O(nlogn)
// 空間複雜度: O(n)
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        set<int> record;
        for( int i = 0 ; i < nums1.size() ; i ++ ){
            record.insert(nums1[i]);
        }
        set<int> resultSet;
        for( int i = 0 ; i < nums2.size() ; i ++ ){
            if(record.find(nums2[i]) != record.end()){
                resultSet.insert(nums2[i]);
            }
        }
        vector<int> resultVector;
        for(set<int>::iterator iter = resultSet.begin(); iter != resultSet.end(); iter ++ ){
            resultVector.push_back(*iter);
        }

        return resultVector;
    }
};
複製程式碼

6. 兩個陣列的交集 II

題目來源於 LeetCode 上第 350 號問題: Intersection of Two Arrays II。

題目描述

給定兩個陣列,編寫一個函式來計算它們的交集。

示例 1:

輸入: nums1 = [1,2,2,1], nums2 = [2,2]
輸出: [2,2]
複製程式碼

示例 2:

輸入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
輸出: [4,9]
複製程式碼

題目解析

與上題 兩個陣列的交集 類似。只不過這裡使用的是 map 。

  • 遍歷 num1,通過 map 容器 record 儲存 num1 的元素與頻率;
  • 遍歷 num2 ,在 record 中查詢是否有相同的元素(該元素的儲存頻率大於 0 ),如果有,用 map 容器resultVector 進行儲存,同時該元素的頻率減一。

動畫描述

兩個陣列的交集 II

程式碼實現

// 時間複雜度: O(nlogn)
// 空間複雜度: O(n)
class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        map<int, int> record;
        for(int i = 0 ; i < nums1.size() ; i ++){
             record[nums1[i]] += 1;
        }
        vector<int> resultVector;
        for(int i = 0 ; i < nums2.size() ; i ++){
            if(record[nums2[i]] > 0){
                resultVector.push_back(nums2[i]);
                record[nums2[i]] --;
            }
        }
        return resultVector;
    }
};
複製程式碼

7. 迴旋鏢的數量

題目來源於 LeetCode 上第 447 號問題: Number of Boomerangs 。

題目描述

給定平面上 n 對不同的點,“迴旋鏢” 是由點表示的元組 (i, j, k) ,其中 ij 之間的距離和 ik 之間的距離相等(需要考慮元組的順序)。

找到所有迴旋鏢的數量。你可以假設 n 最大為 500,所有點的座標在閉區間 [-10000, 10000] 中。

題目解析

n 最大為 500,可以使用時間複雜度為 O(n^2)的演算法。

  • 遍歷所有的點,讓每個點作為一個錨點
  • 然後再遍歷其他的點,統計和錨點距離相等的點有多少個
  • 然後分別帶入 n(n-1) 計算結果並累加到 res 中

注意點:

  • 如果有一個點a,還有兩個點 b 和 c ,如果 ab 和 ac 之間的距離相等,那麼就有兩種排列方法 abc 和 acb ;

  • 如果有三個點b,c,d 都分別和 a 之間的距離相等,那麼有六種排列方法,abc, acb, acd, adc, abd, adb;

  • 如果有 n 個點和點 a 距離相等,那麼排列方式為 n(n-1);

  • 計算距離時不進行開根運算, 以保證精度;

  • 只有當 n 大於等於 2 時,res 值才會真正增加,因為當n=1時,增加量為1 * ( 1 - 1 ) = 0

動畫描述

迴旋鏢的數量

程式碼實現

// 時間複雜度: O(n^2)
// 空間複雜度: O(n)
class Solution {
public:
    int numberOfBoomerangs(vector<pair<int, int>>& points) {
        int res = 0;
        for( int i = 0 ; i < points.size() ; i ++ ){
            // record中儲存 點i 到所有其他點的距離出現的頻次
            unordered_map<int, int> record;
            for(int j = 0 ; j < points.size() ; j ++){
                if(j != i){
                    // 計算距離時不進行開根運算, 以保證精度
                    record[dis(points[i], points[j])] += 1;
                }
            }
            for(unordered_map<int, int>::iterator iter = record.begin() ; iter != record.end() ; iter ++){
                res += (iter->second) * (iter->second - 1);
            }
        }
        return res;
    }

private:
    int dis(const pair<int,int> &pa, const pair<int,int> &pb){
        return (pa.first - pb.first) * (pa.first - pb.first) +
               (pa.second - pb.second) * (pa.second - pb.second);
    }
};
複製程式碼

8. 四數相加 II

題目來源於 LeetCode 上第 454 號問題: 4Sum II 。

題目描述

給定四個包含整數的陣列列表 A , B , C , D ,計算有多少個元組 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0

為了使問題簡單化,所有的 A, B, C, D 具有相同的長度 N,且 0 ≤ N ≤ 500 。所有整數的範圍在 -2^28 到 2^28- 1 之間,最終結果不會超過 2^31 - 1 。

題目解析

Two Sum 極其類似,使用雜湊表來解決問題。

  • 把 A 和 B 的兩兩之和都求出來,在雜湊表中建立兩數之和與其出現次數之間的對映;
  • 遍歷 C 和 D 中任意兩個數之和,只要看雜湊表存不存在這兩數之和的相反數就行了。

動畫描述

四數相加 II

程式碼實現

// 時間複雜度: O(n^2)
// 空間複雜度: O(n^2)
class Solution {
public:
    int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
        unordered_map<int,int> hashtable;
        for(int i = 0 ; i < A.size() ; i ++){
            for(int j = 0 ; j < B.size() ; j ++){
                 hashtable[A[i]+B[j]] += 1;
            }
        }
        int res = 0;
        for(int i = 0 ; i < C.size() ; i ++){
            for(int j = 0 ; j < D.size() ; j ++){
                if(hashtable.find(-C[i]-D[j]) != hashtable.end()){
                    res += hashtable[-C[i]-D[j]];
                }
            }
        }
        return res;
    }
};
複製程式碼

更多內容

幾道和雜湊(雜湊)表有關的面試題

相關文章