【算法】哈希表

1 哈希表理論基礎

1.1 哈希表

哈希表是根據關鍵碼的值而直接進行訪問的數據結構。一般哈希表都是用來快速判斷一個元素是否出現集合裏。

1.2 哈希函數

哈希函數如下圖所示,通過hashCode把名字轉化爲數值,一般hashcode是通過特定編碼方式,可以將其他數據格式轉化爲不同的數值。如果hashCode得到的數值大於tableSize,此時爲了保證映射出來的索引數值都落在哈希表上,會再次對數值做一個取模的操作。

好幾個名字同時映射到哈希表同一個索引下標的位置,叫哈希碰撞

一般有兩種解決方法, 拉鍊法和線性探測法。

拉鍊法

發生衝突的元素都被存儲在鏈表中。拉鍊法就是要選擇適當的哈希表的大小,這樣既不會因爲數組空值而浪費大量內存,也不會因爲鏈表太長而在查找上浪費太多時間。

線性探測法

使用線性探測法,一定要保證tableSize大於dataSize,依靠哈希表中的空位來解決碰撞問題。

例如衝突的位置放了小李,那麼就向下找一個空位放置小王的信息,如圖所示:

1.3 常見的三種哈希結構

  • 數組
  • set (集合)
  • map(映射)

在C++中,set 和 map 分別提供以下三種數據結構,其底層實現以及優劣如下表所示:

集合底層實現是否有序數值是否可以重複能否更改數值查詢效率增刪效率
std::set 紅黑樹 有序 O(log n) O(log n)
std::multiset 紅黑樹 有序 O(logn) O(logn)
std::unordered_set 哈希表 無序 O(1) O(1)

std::unordered_set底層實現爲哈希表,std::set 和std::multiset 的底層實現是紅黑樹,紅黑樹是一種平衡二叉搜索樹,所以key值是有序的,但key不可以修改,改動key值會導致整棵樹的錯亂,所以只能刪除和增加。

當我們要使用集合來解決哈希問題的時候,優先使用unordered_set,因爲它的查詢和增刪效率是最優的,如果需要集合是有序的,那麼就用set,如果要求不僅有序還要有重複數據的話,那麼就用multiset。

映射底層實現是否有序數值是否可以重複能否更改數值查詢效率增刪效率
std::map 紅黑樹 key有序 key不可重複 key不可修改 O(logn) O(logn)
std::multimap 紅黑樹 key有序 key可重複 key不可修改 O(log n) O(log n)
std::unordered_map 哈希表 key無序 key不可重複 key不可修改 O(1) O(1)

map 是一個key-value的數據結構,map中對key是有限制,對value沒有限制的,因爲key的存儲方式使用紅黑樹實現的。std::unordered_map 底層實現爲哈希表,std::map 和std::multimap 的底層實現是紅黑樹。同理,std::map 和std::multimap 的key也是有序的。

1.4 總結

當我們遇到了要快速判斷一個元素是否出現集合裏的時候,就要考慮哈希法

但是哈希法也是犧牲了空間換取了時間,因爲我們要使用額外的數組,set或者是map來存放數據,才能實現快速的查找。

2 有效的字母異位詞*

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

**注意:**若 s 和 t 中每個字符出現的次數都相同,則稱 s 和 t 互爲字母異位詞。

說明: 字符串只包含小寫字母。

示例 1:

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

示例 2:

輸入:s = "rat",t = "car"
輸出:false

思路

  1. 數組
  2. 數組其實就是一個簡單哈希表,而且這道題目中字符串只有小寫字符,那麼就可以定義一個大小爲26的數組record,來記錄字符串s裏字符出現的次數。需要把字符映射到數組索引下標上,因爲字符a到字符z的ASCII是26個連續的數值,所以字符a映射爲下標0,相應的字符z映射爲下標25。

    在遍歷字符串s的時候,**只需要將 s[i] - ‘a’ 所在的元素做+1 操作即可,**這樣就將字符串s中字符出現的次數統計出來了。同樣在遍歷字符串t的時候,對t中出現的字符映射哈希表索引上的數值再做-1的操作。

    最後檢查一下,**record數組如果有的元素不爲零0,說明字符串s和t一定是誰多了字符或者誰少了字符,return false。**最後如果record數組所有元素都爲零0,說明字符串s和t是字母異位詞,return true。

    class Solution:
        def isAnagram(self, s: str, t: str) -> bool:
            record = [0] * 26
            for i in s:
                #並不需要記住字符a的ASCII,只要求出一個相對數值就可以了
                record[ord(i) - ord("a")] += 1
            for i in t:
                record[ord(i) - ord("a")] -= 1
            for i in range(26):
                if record[i] != 0:
                    #record數組如果有的元素不爲零0,說明字符串s和t 一定是誰多了字符或者誰少了字符。
                    return False
            return True
    

    時間複雜度: O(n),空間複雜度: O(1)

  3. defaultdict
  4. defaultdict使得在處理字典時更加方便,無需在訪問不存在的鍵之前檢查鍵是否存在,而是可以直接使用它們。

    class Solution:
        def isAnagram(self, s: str, t: str) -> bool:
            from collections import defaultdict
            # 創建一個 defaultdict,指定默認值爲 int 類型,即默認值爲 0
            s_dict = defaultdict(int)
            t_dict = defaultdict(int)
            for x in s:
                s_dict[x] += 1   # 訪問不存在的鍵不會引發 KeyError,而是使用默認值 0 創建該鍵,可以在不提前設置默認值的情況下遞增不存在的鍵的值
            for x in t:
                t_dict[x] += 1
            return s_dict == t_dict
    
  5. Counter
  6. Counter 是 Python 中 collections 模塊中的一個類,它用於計數可哈希對象(hashable objects)的出現次數。Counter 返回一個字典,其中鍵是可哈希對象,而值是該對象在輸入序列中出現的次數Counter 是一種非常方便的工具,特別適用於統計元素的頻率。

    class Solution(object):
        def isAnagram(self, s: str, t: str) -> bool:
            from collections import Counter
            a_count = Counter(s)
            b_count = Counter(t)
            return a_count == b_count
    

3 兩個數組的交集

題目:給定兩個數組 nums1 和 nums2 ,返回它們的交集 。輸出結果中的每個元素一定是唯一的。我們可以不考慮輸出結果的順序 。

示例 1:

輸入:nums1 = [1,2,2,1], nums2 = [2,2]
輸出:[2]

思路

要求不重複,可以用set

  1. 使用字典和集合
  2. class Solution:
        def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
            from collections import defaultdict
            dict = defaultdict(int) # 使用哈希表存儲一個數組中的所有元素
            for i in nums1: 
                dict[i] += 1
    
        union = set() # 使用集合存儲結果
        for i in nums2:
            if i in dict:
                union.add(i)
    
        return list(union) # 記得轉回list
    

    時間複雜度:O(n),空間複雜度:O(n)

  3. 使用集合*
  4. class Solution:
        def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
            return list(set(nums1) & set(nums2))
    

    tips

    1. 交集操作:&,用於獲取兩個集合的共同元素,返回一個新集合,包含兩個集合中都存在的元素。
    2. 並集操作: |,表示兩個集合的合併,返回包含兩個集合中所有不重複元素的新集合。
    3. 差集操作:-,用於從一個集合中移除另一個集合中包含的元素,返回一個新集合,包含在第一個集合中但不在第二個集合中的元素。
    4. 對稱差集操作:^,用於獲取兩個集合中不重複的元素,返回一個新集合,包含只存在於一個集合中的元素。

思考:爲什麼不直接所有題目都用set?

直接使用set 不僅佔用空間比數組大,而且速度要比數組慢,set把數值映射到key上都要做hash計算。不要小瞧這個耗時,在數據量大的情況,差距是很明顯的。

4 快樂數*

題目:編寫一個算法來判斷一個數 n 是不是快樂數。

「快樂數」 定義爲:

  • 對於一個正整數,每一次將該數替換爲它每個位置上的數字的平方和。
  • 然後重複這個過程直到這個數變爲 1,也可能是 無限循環 但始終變不到 1。
  • 如果這個過程 結果爲 1,那麼這個數就是快樂數。

如果 n 是 快樂數 就返回 true ;不是,則返回 false 。

示例 1:

輸入:n = 19
輸出:true
解釋:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

思路**

題目中說了會無限循環,也就是說求和的過程中,sum會重複出現,這對解題很重要。

當我們遇到了要快速判斷一個元素是否出現集合裏的時候,就要考慮哈希法了。

所以這道題目使用哈希法,來判斷這個sum是否重複出現,如果重複了就是return false, 否則一直找到sum爲1爲止

  1. 集合解法一(divmod
  2. class Solution:
        def isHappy(self, n: int) -> bool:        
            record = set()
    
        while True:
            n = self.get_sum(n)
            if n == 1:
                return True         
            # 如果中間結果重複出現,說明陷入死循環了,該數不是快樂數
            if n in record:
                return False
            else:
                record.add(n)
    
    def get_sum(self,n: int) -> int: 
        new_num = 0
        while n:
            n, r = divmod(n, 10)
            new_num += r ** 2
        return new_num
    

  3. 集合解法二(字符串↔int
  4. class Solution:
       def isHappy(self, n: int) -> bool:
           record = set()
           while n not in record:
               record.add(n)
               new_num = 0
               n_str = str(n)
               for i in n_str:
                   new_num+=int(i)**2
               if new_num==1: return True
               else: n = new_num
           return False
    
  5. 集合解法三(精簡**
  6. class Solution:
       def isHappy(self, n: int) -> bool:
           seen = set()
           while n != 1:
               n = sum(int(i) ** 2 for i in str(n))
               if n in seen:
                   return False
               seen.add(n)
           return True
    

5 兩數之和

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

你可以假設每種輸入只會對應一個答案。但是,數組中同一個元素在答案裏不能重複出現。

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

示例 1:

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

示例 2:

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

思路

本題需要一個集合來存放我們遍歷過的元素,然後在遍歷數組的時候去詢問這個集合,某元素是否遍歷過,也就是是否出現在這個集合。因爲本題不僅要知道元素有沒有遍歷過,還要知道這個元素對應的下標,需要使用 key value結構來存放,key來存元素,value來存下標,那麼使用map正合適

  1. 字典法
  2. class Solution:
        def twoSum(self, nums: List[int], target: int) -> List[int]:
            records = dict()
            for index, value in enumerate(nums):  
                if target - value in records:   # 遍歷當前元素,並在map中尋找是否有匹配的key
                    return [records[target- value], index]
                records[value] = index    # 如果沒找到匹配對,就把訪問過的元素和下標加入到map中
            return []
    
  3. 集合法
  4. class Solution:
        def twoSum(self, nums: List[int], target: int) -> List[int]:
            seen = set() #創建一個集合來存儲我們目前看到的數字
            for idx, val in enumerate(nums):
                if target - val in seen:
                    return [nums.index(target - val), idx]
                seen.add(val)
    

    時間複雜度:O(n),空間複雜度:O(n)

6 四數相加

題目:給你四個整數數組 nums1nums2nums3 和 nums4 ,數組長度都是 n ,請你計算有多少個元組 (i, j, k, l) 能滿足:nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:

輸入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
輸出:2
解釋:
兩個元組如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

思路*

本題解題步驟:

  1. 首先定義一個unordered_map,key放a和b兩數之和,value 放a和b兩數之和出現的次數。
  2. 遍歷大A和大B數組,統計兩個數組元素之和,和出現的次數,放到map中。
  3. 定義int變量count,用來統計 a+b+c+d = 0 出現的次數。
  4. 在遍歷大C和大D數組,找到如果 0-(c+d) 在map中出現過的話,就用count把map中key對應的value也就是出現次數統計出來。
  5. 最後返回統計值 count 就可以了
class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        dict, cnt = defaultdict(int), 0
        for i in nums1:
            for j in nums2:
                dict[i + j] += 1 # 初始默認是0
        for i in nums3:
            for j in nums4:
                cnt += dict[-(i + j)] # 沒有的話就是0
        return cnt

時間複雜度:O(n^2),空間複雜度:O(n^2)

7 贖金信

題目:給你兩個字符串:ransomNote 和 magazine ,判斷 ransomNote 能不能由 magazine 裏面的字符構成。

如果可以,返回 true ;否則返回 false 。

magazine 中的每個字符只能在 ransomNote 中使用一次。

示例 1:

輸入:ransomNote = "a", magazine = "b"
輸出:false

示例 2:

輸入:ransomNote = "aa", magazine = "aab"
輸出:true

思路

  1. defaultdict法
  2. class Solution:
        def canConstruct(self, ransomNote: str, magazine: str) -> bool:
            dict = defaultdict(int)
            for s in magazine:
                dict[s] += 1
            for s in ransomNote:
                dict[s] -= 1
            # for val in dict.values():
            #     if val < 0:
            #         return False
            # return True
            return all(val >= 0 for val in dict.values())
    
  3. Counter法*
  4. from collections import Counter
    class Solution:
        def canConstruct(self, ransomNote: str, magazine: str) -> bool:
            return not Counter(ransomNote) - Counter(magazine)
    
  5. count法*
  6. class Solution:
        def canConstruct(self, ransomNote: str, magazine: str) -> bool:
            return all(ransomNote.count(c) <= magazine.count(c) for c in set(ransomNote))
    

8 三數之和*

題目:給你一個整數數組 nums ,判斷是否存在三元組 [nums[i], nums[j], nums[k]] 滿足 i != ji != k 且 j != k ,同時還滿足 nums[i] + nums[j] + nums[k] == 0 。請你返回所有和爲 0 且不重複的三元組。

示例 1:

輸入:nums = [-1,0,1,2,-1,-4]
輸出:[[-1,-1,2],[-1,0,1]]

思路

  1. 哈希法
  2. 去重很容易超時!需要剪枝(有點噁心就跳過吧)

  3. 雙指針法
  4. 這題雙指針法比哈希法高效一點。首先將數組排序,有一層for循環,i從下標0的地方開始,同時定一個下標left 定義在i+1的位置上,定義下標right 在數組結尾的位置上。

    依然還是在數組中找到 abc 使得a + b +c =0,我們這裏相當於 a = nums[i],b = nums[left],c = nums[right]。

    接下來如何移動left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就說明此時三數之和大了,因爲數組是排序後了,所以right下標就應該向左移動,這樣才能讓三數之和小一些。

    如果 nums[i] + nums[left] + nums[right] < 0 說明此時 三數之和小了,left 就向右移動,才能讓三數之和大一些,直到left與right相遇爲止。

    class Solution:
        def threeSum(self, nums: List[int]) -> List[List[int]]:
            nums.sort()
            res = []
            # res = set()
            for i in range(len(nums) - 2):
                left, right = i + 1, len(nums) - 1
                # 剪枝
                if nums[i] > 0:
                    break
                elif nums[i] + nums[left] > 0:
                    break
                if i > 0 and nums[i] == nums[i - 1]: # a去重
                    continue
                while left < right:
                    if nums[i] + nums[left] + nums[right] < 0:
                        left += 1
                    elif nums[i] + nums[left] + nums[right] > 0:
                        right -= 1
                    else:
                        res.append([nums[i], nums[left], nums[right]])
                        # 去重邏輯應該放在找到一個三元組之後,對b 和 c去重
                        while left < right and nums[left] == nums[left + 1]:
                            left += 1
                        while left < right and nums[right] == nums[right - 1]:
                            right -= 1
    
                    # res.add((nums[i], nums[left], nums[right]))
    
                    left += 1
                    right -= 1
        
        return res
        # return list(res)
    

9 四數之和**(經典

題目:給定一個包含 n 個整數的數組 nums 和一個目標值 target,判斷 nums 中是否存在四個元素 a,b,c 和 d ,使得 a + b + c + d 的值與 target 相等?找出所有滿足條件且不重複的四元組。

示例 1:

輸入:nums = [1,0,-1,0,-2,2], target = 0
輸出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

思路**

注意細節:不要判斷nums[k] > target 就返回了,三數之和可以通過 nums[i] > 0 就返回了,因爲 0 已經是確定的數了,四數之和這道題目 target是任意值。比如:數組是[-4, -3, -2, -1]target-10,不能因爲-4 > -10而跳過。

  1. 雙指針法
  2. class Solution:
        def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
            nums.sort()
            res = []
            for i in range(len(nums)):
                if i > 0 and nums[i] == nums[i - 1]: # a去重
                    continue
                if nums[i] > target and nums[i] >= 0: # 剪枝(optional
                    break
                for j in range(i+1, len(nums)):
                    left, right = j + 1, len(nums) - 1
                    sum = nums[i] + nums[j]
                    if sum > target and sum >= 0: # 剪枝(optional
                        break
                    if j > i + 1 and nums[j] == nums[j - 1]: # b去重
                        continue
                    while left < right:
                        if sum + nums[left] + nums[right] < target:
                            left += 1
                        elif sum + nums[left] + nums[right] > target:
                            right -= 1
                        else:
                            res.append([nums[i], nums[j], nums[left], nums[right]])
                            while left < right and nums[left] == nums[left + 1]:
                                left += 1
                            while left < right and nums[right] == nums[right - 1]:
                                right -= 1
                            left += 1
                            right -= 1
    
        return res
    

  3. 字典法(適用於n數之和題目/要求輸出爲值*
  4. class Solution(object):
        def fourSum(self, nums, target):
            # 創建一個字典來存儲輸入列表中每個數字的頻率
            freq = defaultdict(int)
            for i in nums:
                freq[i] += 1
    
        # 創建一個集合來存儲最終答案,並遍歷4個數字的所有唯一組合
        ans = set()
        for i in range(len(nums)):
            for j in range(i + 1, len(nums)):
                for k in range(j + 1, len(nums)):
                    val = target - (nums[i] + nums[j] + nums[k])
                    if val in freq:
                        # 確保沒有重複
                        count = (nums[i] == val) + (nums[j] == val) + (nums[k] == val)
                        if freq[val] &gt; count:
                            ans.add(tuple(sorted([nums[i], nums[j], nums[k], val])))
        
        return [list(x) for x in ans]
    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章