【演算法】雜湊表法四部曲

Nemo&發表於2021-03-27

雜湊表

雜湊表(Hash table,也叫雜湊表),是根據關鍵碼值(Key value)而直接進行訪問的資料結構。也就是說,它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度。這個對映函式叫做雜湊函式,存放記錄的陣列叫做雜湊表。

給定表M,存在函式f(key),對任意給定的關鍵字值key,代入函式後若能得到包含該關鍵字的記錄在表中的地址,則稱表M為雜湊(Hash)表,函式f(key)為雜湊(Hash) 函式。

關鍵碼值與地址一一對映

適用場景

適用於關鍵字與某一值一一對應,即 可使用鍵值對map,而hashmap是鍵值對中較好的實現類

關鍵詞:一一對應

遍歷雜湊表的四種方式

public static void main(String[] args) {
  Map<String,String> map=new HashMap<String,String>();
        map.put("1", "value1");
        map.put("2", "value2");
        map.put("3", "value3");
        map.put("4", "value4");
        
        //第一種:普通使用,二次取值
        // 遍歷鍵,取出值
        System.out.println("\n通過Map.keySet遍歷key和value:");  
        for(String key:map.keySet()) {
          System.out.println("Key: "+key+" Value: "+map.get(key));
        }
        
        //第二種
        // 使用Map.entrySet()的迭代器
        System.out.println("\n通過Map.entrySet使用iterator遍歷key和value: ");  
        Iterator map1it=map.entrySet().iterator();
        while(map1it.hasNext()) {
          Map.Entry<String, String> entry=(Entry<String, String>) map1it.next();
          System.out.println("Key: "+entry.getKey()+" Value: "+entry.getValue());
        }
        
        //第三種:推薦,尤其是容量大時
        // foreach
        System.out.println("\n通過Map.entrySet遍歷key和value");  
        for(Map.Entry<String, String> entry: map.entrySet()) {
          System.out.println("Key: "+ entry.getKey()+ " Value: "+entry.getValue());
        }
        
        //第四種  
        // 遍歷value
        System.out.println("\n通過Map.values()遍歷所有的value,但不能遍歷key");  
        for(String v:map.values()) {
          System.out.println("The value is "+v);
        }
 }

輸出結果:

通過Map.keySet遍歷key和value:
Key: 1 Value: value1
Key: 2 Value: value2
Key: 3 Value: value3
Key: 4 Value: value4

通過Map.entrySet使用iterator遍歷key和value: 
Key: 1 Value: value1
Key: 2 Value: value2
Key: 3 Value: value3
Key: 4 Value: value4

通過Map.entrySet遍歷key和value
Key: 1 Value: value1
Key: 2 Value: value2
Key: 3 Value: value3
Key: 4 Value: value4

通過Map.values()遍歷所有的value,但不能遍歷key
The value is value1
The value is value2
The value is value3
The value is value4

【推薦】使用entrySet遍歷Map類集合KV,而不是keySet方式進行遍歷。
說明:keySet 其實是遍歷了2次,一次是轉為Iterator物件,另一次是從hashMap中取出key所對應的value。而entrySet只是遍歷了一次就把key和value都放到了entry中,效
率更高。如果是JDK8,使用Map.foreach方法。
正例:values()返回的是V值集合,是一個list集合物件; keySet()返回的是K值集合,是一個Set集合物件; entrySet()返回的是K-V值組合集合。

記錄陣列中元素出現頻數

遍歷nums1,使用雜湊表儲存關鍵字,以及他們出現的次數

方法一:遇到空的就賦初值,非空就+1

// 1. 遍歷nums1,使用雜湊表儲存關鍵字,以及他們出現的次數
Map<Integer, Integer> map = new HashMap<>();

for (int i = 0; i < nums1.length; i++) {
    if (map.get(nums1[i]) != null) {
        map.put(nums1[i], map.get(nums1[i])+1);
    } else {
        map.put(nums1[i], 1);
    }
}

方法二:使用getOrDefault()

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

for (int num : nums1) {
    int count = map.getOrDefault(num, 0) + 1;
    map.put(num, count);
}

方法三:如果元素固定,那麼我們可以就使用一個一維陣列來儲存他們出現的頻數。這也是雜湊表法。
遍歷字串 p,記錄字元頻數

int[] sArr = new int[26];

for (int i = 0; i < p.length(); i++) {
    sArr[s.charAt(i) - 'a']++;  
}

方法四:如果要求元素只出現一次 或者判斷是否有重複元素,那就可以用雜湊集合

Set<Integer, Integer> set = new HashSet<Integer>();

for (int num : nums1) {
    // 新增此元素至 Set,加入失敗那就代表有重複
    if(!set.add(num)) {
        return false;
    }
}

例項

1. 兩數之和

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

你可以假設每種輸入只會對應一個答案。但是,陣列中同一個元素不能使用兩遍。

你可以按任意順序返回答案。

 

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

示例 2:
輸入:nums = [3,2,4], target = 6
輸出:[1,2]

示例 3:
輸入:nums = [3,3], target = 6
輸出:[0,1]

答案

class Solution {
    public int[] twoSum(int[] nums, int target) {

        // 雜湊表用來存放(關鍵字,下標)
        Map<Integer, Integer> map = new HashMap<>();

        // 遍歷陣列,每次遇到一個元素判斷雜湊表裡面有沒有與其對應的target - nums[i]元素
        // 如果有就返回下標,如果沒有就把它的關鍵字和下標放進去,
        for (int i = 0; i < nums.length; i++) {

            Integer index = map.get(target - nums[i]);
            if (index != null) {
                return new int[]{index, i};
            } else {
                map.put(nums[i], i);
            }
        }
        return null;
    }
}

有效的字母異位詞

給定兩個字串 s 和 t ,編寫一個函式來判斷 t 是否是 s 的字母異位詞。

示例 1:

輸入: s = "anagram", t = "nagaram"
輸出: true
示例 2:

輸入: s = "rat", t = "car"
輸出: false
說明:
你可以假設字串只包含小寫字母。

進階:
如果輸入字串包含 unicode 字元怎麼辦?你能否調整你的解法來應對這種情況?

答案

方法一:排序

t 是 s 的異位詞等價於「兩個字串排序後相等」。因此我們可以對字串 s 和 t 分別排序,看排序後的字串是否相等即可判斷。此外,如果 s 和 t 的長度不同,t 必然不是 s 的異位詞。

Java

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        char[] str1 = s.toCharArray();
        char[] str2 = t.toCharArray();
        Arrays.sort(str1);
        Arrays.sort(str2);
        return Arrays.equals(str1, str2);
    }
}

複雜度分析

  • 時間複雜度:\(O(n \log n)\),其中 n 為 s 的長度。排序的時間複雜度為 \(O(n\log n)\),比較兩個字串是否相等時間複雜度為 \(O(n)\),因此總體時間複雜度為 \(O(n \log n+n)=O(n\log n)\)

  • 空間複雜度:\(O(\log n)\)。排序需要 \(O(\log n)\) 的空間複雜度。注意,在某些語言(比如 Java & JavaScript)中字串是不可變的,因此我們需要額外的 \(O(n)\) 的空間來拷貝字串。但是我們忽略這一複雜度分析,因為:

這依賴於語言的細節;
這取決於函式的設計方式,例如,可以將函式引數型別更改為 char[]。

方法二:雜湊表

前面我們說過了關鍵碼值與地址一一對映,就可以稱為雜湊表(即 雜湊表),所以此處的方法也可以稱為雜湊表法。

從另一個角度考慮,t 是 s 的異位詞等價於「兩個字串中字元出現的種類和次數均相等」。由於字串只包含 26 個小寫字母,因此我們可以維護一個長度為 26 的頻次陣列 \(\textit{table}\),先遍歷記錄字串 s 中字元出現的頻次,然後遍歷字串 t,減去 \(\textit{table}\) 中對應的頻次,如果出現 \(\textit{table}[i]<0\),則說明 t 包含一個不在 s 中的額外字元,返回 \(\text{false}\) 即可。

Java

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        int[] table = new int[26];
        for (int i = 0; i < s.length(); i++) {
            table[s.charAt(i) - 'a']++;
        }
        for (int i = 0; i < t.length(); i++) {
            table[t.charAt(i) - 'a']--;
            if (table[t.charAt(i) - 'a'] < 0) {
                return false;
            }
        }
        return true;
    }
}

對於進階問題,\(\text{Unicode}\) 是為了解決傳統字元編碼的侷限性而產生的方案,它為每個語言中的字元規定了一個唯一的二進位制編碼。而 \(\text{Unicode}\) 中可能存在一個字元對應多個位元組的問題,為了讓計算機知道多少位元組表示一個字元,面向傳輸的編碼方式的 \(\text{UTF}-8\)\(\text{UTF}-16\) 也隨之誕生逐漸廣泛使用,具體相關的知識讀者可以繼續查閱相關資料擴充視野,這裡不再展開。

回到本題,進階問題的核心點在於「字元是離散未知的」,因此我們用雜湊表維護對應字元的頻次即可。同時讀者需要注意 \(\text{Unicode}\) 一個字元可能對應多個位元組的問題,不同語言對於字串讀取處理的方式是不同的。

Java

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) {
            return false;
        }
        Map<Character, Integer> table = new HashMap<Character, Integer>();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            table.put(ch, table.getOrDefault(ch, 0) + 1);
        }
        for (int i = 0; i < t.length(); i++) {
            char ch = t.charAt(i);
            table.put(ch, table.getOrDefault(ch, 0) - 1);
            if (table.get(ch) < 0) {
                return false;
            }
        }
        return true;
    }
}

複雜度分析

  • 時間複雜度:\(O(n)\),其中 n 為 s 的長度。

  • 空間複雜度:\(O(S)\),其中 S 為字符集大小,此處 \(S=26\)

劍指 Offer 61. 撲克牌中的順子

從撲克牌中隨機抽5張牌,判斷是不是一個順子,即這5張牌是不是連續的。2~10為數字本身,A為1,J為11,Q為12,K為13,而大、小王為 0 ,可以看成任意數字。A 不能視為 14。

示例 1:

輸入: [1,2,3,4,5]
輸出: True

示例 2:

輸入: [0,0,1,2,5]
輸出: True

答案

class Solution {
    public boolean isStraight(int[] nums) {
        Set<Integer> repeat = new HashSet<>();
        int max = 0, min = 14;
        for(int num : nums) {
            if(num == 0) continue; // 跳過大小王
            max = Math.max(max, num); // 最大牌
            min = Math.min(min, num); // 最小牌
            // 新增此牌至 Set,加入失敗那就代表有重複
            if(!repeat.add(num)) return false; // 若有重複,提前返回 false
             
        }
        return max - min + 1 <= 5; // 最大牌 - 最小牌 + 1 <= 5 則可構成順子,因為包含有0、0、0、9、11的情況
    }
}

相關文章