Leetcode之哈希查找

1. 哈希查找

本质上就是个搜索,但是可以将在一个集合中查找一个元素的时间复杂度降低到O(1)。python中常用的有以下方式:

  • set
  • dict
  • 数组模拟

2. 相关算法题

2.1. Leetcode 771 宝石与石头

给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。 stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。

字母区分大小写,因此 “a” 和 “A” 是不同类型的石头。

示例1

输入:jewels = “aA”, stones = “aAAbbbb”
输出:3

示例 2:

输入:jewels = “z”, stones = “ZZ”
输出:0

提示:

  • 1 <= jewels.length, stones.length <= 50
  • jewels 和 stones 仅由英文字母组成
  • jewels 中的所有字符都是 唯一的
  • 思路解析
  1. 可以尝试使用set,因为set的底层是哈希表,所以查找的时间复杂度是O(1)。
  2. 也可以用list构建一个虚拟的哈希表,然后查找。

class Solution:
    # 利用set的哈希特性
    def numJewelsInStones1(self, jewels: str, stones: str) -> int:
        jewels = set(jewels)
        
        count = 0
        for i in range(len(stones)):
            if stones[i] in jewels:
                count += 1
        return count
    
    # 利用数组代替hash表
    def numJewelsInStones(self,jewels:str,stones:str) -> int:
        count = 0
        counts = [0 for i in range(58)] # 虚拟哈希表
        for c in jewels:
            counts[ord(c) - ord('A')] = 1
        
        for c in stones:
            count += counts[ord(c) - ord('A')]
        return count
2.2. Leetcode 128 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:

输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

提示:

  • 0 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9
  • 思路解析

有两种思路:

  1. 先排序,然后遍历一遍,找到最长的连续序列。因为要求为O(n),所以只能使用计数+基数排序的方式,时间复杂度为O(n)。
  2. 哈希表,先将所有的数字存入哈希表,然后遍历一遍,找到最长的连续序列。

class Solution:
    def counter_sort(self,nums,n:int,base:int=0):
        tmp_res = [[] for _ in range(10)]
        for num in nums:
            index = num//(10**base)%10
            tmp_res[index].append(num)
        res = []
        for tr in tmp_res:
            res.extend(tr)
        return res
    
    
    def base_sort(self,nums:List[int]):
        if len(nums) == 0: return []
        max_num = max(nums)
        num_length = len(str(max_num))
        for i in range(num_length):
            nums = self.counter_sort(nums,num_length,i)
        return nums
    
    def longestConsecutive(self, nums: List[int]) -> int:
        """
        基数排序 + 计数排序
        """
        
        
        if len(nums) == 0:return 0
        if len(nums) == 1:return 1
        
        nums1 = [num for num in nums if num >= 0]
        nums2 = [- num for num in nums if num < 0]
        
        nums1 = self.base_sort(nums1) if len(nums1) > 0 else nums1
        nums2 = self.base_sort(nums2) if len(nums2) > 0 else nums2
        nums2 = [-num for num in nums2]
        nums2.reverse()
        nums2.extend(nums1)
        nums = nums2
        
        
        output = []
        setnums = set(nums)
        for item in nums:
            if item not in setnums:
                output.append(item)
        output = []
        i = 0
        while(i<len(nums)):
            while i<len(nums)-1 and nums[i+1] - nums[i] ==0:
                i += 1
            output.append(nums[i])
            i += 1
        
        
        nums = output
        l = 0
        r = 0
        lengths = [0]
        while(r<len(nums) and l<=r):
            while(r<len(nums)-1 and nums[r+1]-nums[r] == 1):
                r += 1
            lengths.append(r-l+1)
            
            r += 1
            l = r
        return max(lengths)

哈希表思路,先将nums存放到一个set中,因为判断元素是否在set中的时间复杂度为O(1),之后只需要针对每个元素,判断这个元素之后的元素是否连续就可以了。


class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        nums_set = set(nums)
        n = len(nums)
        if n<2:return n
        i = 0
        max_num = 1
        while(i<n):
            if nums[i] - 1 in nums_set:
                i += 1
                continue
            tmp_num = 1
            j = i
            while(j<n and (nums[j]+tmp_num) in nums_set):
                tmp_num += 1
            max_num =  tmp_num if tmp_num > max_num else max_num
            i += 1
        return max_num

2.2.1. Leetcode 389 找不同

给定两个字符串 s 和 t ,它们只包含小写字母。

字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。

请找出在 t 中被添加的字母。

示例 1:

输入:s = "abcd", t = "abcde"
输出:"e"
解释:'e' 是那个被添加的字母。

示例 2:

输入:s = "", t = "y"
输出:"y"

提示:

0 <= s.length <= 1000
t.length == s.length + 1
s 和 t 只包含小写字母
  • 思路解析

hash map,在python中其实就是字典


class Solution:
    def findTheDifference(self, s: str, t: str) -> str:
        hash_map = {}
        for s_ in s:
            if s_ not in hash_map:
                hash_map[s_] = 1
            else:
                hash_map[s_] += 1
        
        for t_ in t:
            if t_ not in hash_map:
                return t_
            else:
                hash_map[t_] -= 1
        for k in hash_map:
            if hash_map[k] < 0:
                return k

2.3. leetcode 554 砖墙

你的面前有一堵矩形的、由 n 行砖块组成的砖墙。这些砖块高度相同(也就是一个单位高)但是宽度不同。每一行砖块的宽度之和相等。

你现在要画一条 自顶向下 的、穿过 最少 砖块的垂线。如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。

给你一个二维数组 wall ,该数组包含这堵墙的相关信息。其中,wall[i] 是一个代表从左至右每块砖的宽度的数组。你需要找出怎样画才能使这条线 穿过的砖块数量最少 ,并且返回 穿过的砖块数量 。

  • 示例1

输入:wall = [[1,2,2,1],[3,1,2],[1,3,2],[2,4],[3,1,2],[1,3,1,1]]
输出:2
  • 示例2

输入:wall = [[1],[1],[1]]
输出:3

  • 提示

n == wall.length
1 <= n <= 104
1 <= wall[i].length <= 104
1 <= sum(wall[i].length) <= 2 * 104
对于每一行 i ,sum(wall[i]) 是相同的
1 <= wall[i][j] <= 231 - 1


  • 思路解析

其实就是找到每一行的缝隙,然后找到最多的缝隙,然后用总行数减去最多的缝隙就是最少的穿过的砖块数量。

需要注意的一点,每行的最后一个缝隙一定是最右侧边界,它是不能算的,因为题目要求不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。


class Solution:
    def leastBricks(self, wall: List[List[int]]) -> int:
        n = len(wall)
        hash_map = {}
        
        for i in range(n):
            sum_ = 0
            for j in range(len(wall[i])-1): # 最后一块砖不算,如果算了此时的sum_一定是最右侧的边界,不符合题意
                sum_ += wall[i][j]
                if sum_ not in hash_map:
                    hash_map[sum_] = 1
                else:
                    hash_map[sum_] += 1
        return len(wall) - (max(hash_map.values()) if len(hash_map) > 0 else 0)
2.4. leetcode 205 同构字符串

给定两个字符串 s 和 t ,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

示例 1:

输入:s = "egg", t = "add"
输出:true

示例 2:

输入:s = "foo", t = "bar"
输出:false

示例 3:

输入:s = "paper", t = "title"
输出:true

提示:

1 <= s.length <= 5 * 104
t.length == s.length
s 和 t 由任意有效的 ASCII 字符组成
  • 思路解析

其实就是一个哈希表的问题,只要保证s中的字符和t中的字符一一对应就可以了。为了保证正反的映射,我们可以用两个哈希表,一个记录s到t的映射,一个记录t到s的映射。


class Solution:
    def isIsomorphic(self, s: str, t: str) -> bool:
        maps_s2t = {}
        maps_t2s = {}

        for s_char, t_char in zip(s, t):
            if (
                maps_s2t.get(s_char, t_char) != t_char
                or maps_t2s.get(t_char, s_char) != s_char
            ):
                return False
            maps_s2t[s_char],maps_t2s[t_char] = t_char,s_char
        return True
2.5. Leetcode 290 单词规律

给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。

这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。

示例1:

输入: pattern = "abba", s = "dog cat cat dog"
输出: true

示例 2:

输入:pattern = "abba", s = "dog cat cat fish"
输出: false

示例 3:

输入: pattern = "aaaa", s = "dog cat cat dog"
输出: false

提示:

1 <= pattern.length <= 300
pattern 只包含小写英文字母
1 <= s.length <= 3000
s 只包含小写英文字母和 ' '
s 不包含 任何前导或尾随对空格
s 中每个单词都被 单个空格 分隔
  • 思路解析
    这个的思路和205题一样,也是一个哈希表的问题,只要保证pattern中的字符和s中的单词一一对应就可以了。为了保证正反的映射,我们可以用两个哈希表,一个记录pattern到s的映射,一个记录s到pattern的映射。

class Solution:
    def wordPattern(self, pattern: str, s: str) -> bool:
        p2s, s2p = {}, {}
        if len(pattern) != len(s.split(' ')):return False
        for p, s_word in zip(pattern, s.split(" ")):
            if p2s.get(p, s_word) != s_word or s2p.get(s_word, p) != p:
                return False
            p2s[p], s2p[s_word] = s_word, p
        return True


2.6. Leetcode 242 有效的字母异位词

有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

示例 1:

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

示例 2:

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

提示:

1 <= s.length, t.length <= 5 * 104
s 和 t 仅包含小写字母

进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

  • 思路解析


class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        hash_map = {}
        for s_char in s:
            if s_char not in hash_map:
                hash_map[s_char] = 1
            else:
                hash_map[s_char] += 1

        for t_char in t:
            if t_char not in hash_map:
                return False
            else:
                hash_map[t_char] -= 1
                
        for k,v in hash_map.items():
            if v>0 or v<0:
                return False
        return True

2.7. Leetcode 49 字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:

输入: strs = [""]
输出: [[""]]

示例 3:

输入: strs = ["a"]
输出: [["a"]]

提示:

1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i] 仅包含小写字母

Solution

  • 思路解析

可以用排序后的字符串作为key,原字符串作为value,存入哈希表中,最后返回哈希表的value即可。


class Solution:
    def sort_strs(self,s:str):
        return ''.join(sorted(s))
    
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        res = {}
        for s in strs:
            sorted_s = self.sort_strs(s)
            if sorted_s not in res:
                res[sorted_s] = [s]
            else:
                res[sorted_s].append(s)
        
        res_list = []
        for s_list in res.values():
            res_list.append(s_list)
        return res_list

另一种方式,将每个字母的数量作为key,原字符串作为value,存入哈希表中,最后返回哈希表的value即可。


class Solution:
    def sort_strs(self,s:str):
        return ''.join(sorted(s))
    
    def countmap_strs(self,s:str):
        res = [0]*26
        for s_char in s:
            res[ord(s_char)-ord('a')] += 1
        return str(res)
    
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        res = {}
        for s in strs:
            sorted_s = self.countmap_strs(s)
            if sorted_s not in res:
                res[sorted_s] = [s]
            else:
                res[sorted_s].append(s)
        
        res_list = []
        for s_list in res.values():
            res_list.append(s_list)
        return res_list

2.8. Leetcode 560 和为K的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的连续子数组的个数 。

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

示例 2:

输入:nums = [1,2,3], k = 3
输出:2

提示:

1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
  • 思路解析

前缀和+线性查找是我们最容易想到的方法,但是时间复杂度为O(n^2),不符合题意。

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        sums = [0]*(n+1)
        for i in range(1,n+1):
            sums[i] = sums[i-1] + nums[i-1]
        
        count = 0
        for i in range(1,n+1):
            for j in range(i,n+1):
                if sums[j] - sums[i-1] == k:
                    count += 1
        return count

我们可以用哈希表来优化,将前缀和作为key,出现的次数作为value,这样我们就可以在O(1)的时间复杂度内找到前缀和为k的子数组的个数。

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        sums = [0]*(n+1)
        for i in range(1,n+1):
            sums[i] = sums[i-1] + nums[i-1]
        
        count = 0
        hash_map = {}
        for i in range(1,n+1):
            if sums[i] - k in hash_map:
                count += hash_map[sums[i] - k]
            if sums[i] not in hash_map:
                hash_map[sums[i]] = 1
            else:
                hash_map[sums[i]] += 1
        return count

内存还可以优化,我们可以用一个变量来记录前缀和为k的子数组的个数,这样就不需要哈希表了。

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        
        n = len(nums)
        if n == 1:
            if nums[0] == k:return 1
            else: return 0
        
        res = 0
        
        map = {}
        map[0] = 1
        
        sum_ = 0
        for i in range(n):
            sum_ += nums[i]
            diff = sum_-k # 当前和为sum,则如果存在前缀和为sum-k的,那么就有map[sum-k]个子数组的和为k
            if diff in map:
                res += map[diff] # 如果存在前缀和为diff的,那么就有map[diff]个子数组的和为k
            map[sum_] = map.get(sum_,0) + 1 # 前缀和为sum的个数+1
        return res
2.9. Leetcode 41 缺失的第一个正数

缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例 1:

输入:nums = [1,2,0]
输出:3

示例 2:

输入:nums = [3,4,-1,1]
输出:2

示例 3:

输入:nums = [7,8,9,11,12]
输出:1

提示:

1 <= nums.length <= 5 * 105
-231 <= nums[i] <= 231 - 1

Solution

  • 思路解析

我们可以用哈希表来记录每个数字是否出现过,然后从1开始遍历,找到第一个没有出现过的数字即可。


class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums) + 1
        nums = set(nums)
        for i in range(1,n):
            if i not in nums:
                return i
        return n

但是这样并不满足进阶要求。如何改进呢,我们重新审视题干,可以发现我们要求查找的是正整数,对于正整数我们可以用下标的信息来存储。这样的话就不会有额外空间,我们来验证一下这个思路的可行性。

nums中的数字分为以下几类

  1. 小于等于0的,这部分不用考虑,因为题干中要求找到的是正整数,为了让数据一致,我们可以把这部分值变成大于n的值。
  2. 大于0小于等于n的,这部分可以用下标表示,为了不冲掉原始信息,我们可以将numsi看作下标,然后将nums[nums[i]-1]对应的值置为负数,表示nums[i]出现过;
  3. 大于n的,这部分无法用下标表示,而且如果出现大于n的值,一定表示在此之前已经有缺失的正整数,所以可以保持不变。
class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums)
        for i in range(n):
            if nums[i] <=0:
                nums[i] = n+1
        for i in range(n):
            # nums[i]作为下标
            subscript = abs(nums[i])
            if subscript <= n:
                nums[subscript-1] = -abs(nums[subscript-1]) # 为什么不能直接-nums[subscript-1]?因为一个值有可能出现多次,这时两次操作就有可能把值还原为正的,这样就无法有效过滤掉这个值。
        for i in range(n):
            if nums[i] > 0: return i+1
            
        return n+1
2.10. Leetcode 1122 数组的相对排序

数组的相对排序
给你两个数组,arr1 和 arr2,arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中。

对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。

示例 1:

输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]

示例 2:

输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6]
输出:[22,28,8,6,17,44]

提示:

1 <= arr1.length, arr2.length <= 1000
0 <= arr1[i], arr2[i] <= 1000
arr2 中的元素 arr2[i]  各不相同 
arr2 中的每个元素 arr2[i] 都出现在 arr1 中
  • 思路解析

实际上,这就是相当于两个排序,我们把arr1中数据分为在和不在arr2的两个部分,对第一个部分,两个元素之间的大小比较规则是根据arr2中的相对顺序,对于第二个部分,进行正常的比较。所以,我们可以用任意一种排序算法来实现。



class Solution:
    def __init__(self) -> None:
        self.arr2_indexes = {}

    def compare(self, a: int, b: int):
        if self.arr2_indexes[a] < self.arr2_indexes[b]:  # 小于
            return -1
        elif self.arr2_indexes[a] == self.arr2_indexes[b]:  # 等于
            return 0
        else:
            return 1  # 大于

    def mergesort(
        self, nums: List[int], low: int, high: int, temp: List[int], prefix: bool
    ):
        n = len(nums)
        if low >= high:
            return
        mid = low + (high - low) // 2
        

        self.mergesort(nums, low, mid, temp, prefix)
        self.mergesort(nums, mid + 1, high, temp, prefix)
        
        for i in range(low, high + 1):
            temp[i] = nums[i]

        i = low
        j = mid + 1

        for k in range(low, high + 1):
            if i > mid:
                nums[k] = temp[j]
                j += 1
            elif j > high:
                nums[k] = temp[i]
                i += 1
            elif (
                (self.compare(temp[i], temp[j]) in [-1, 0])
                if prefix
                else (temp[i] <= temp[j])
            ):
                nums[k] = temp[i]
                i += 1
            else:
                nums[k] = temp[j]
                j += 1

    def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:
        for i in range(len(arr2)):
            self.arr2_indexes[arr2[i]] = i
        arr2_set = set(arr2)
        prefix = []
        suffix = []
        for item in arr1:
            if item in arr2_set:
                prefix.append(item)
            else:
                suffix.append(item)

        self.mergesort(prefix, 0, len(prefix) - 1, [0] * len(prefix), True)
        self.mergesort(suffix, 0, len(suffix) - 1, [0] * len(suffix), False)

        prefix.extend(suffix)
        return prefix

另外一种思路,我们可以把arr1中的元素都用字典存储起来其数量,然后遍历arr2中的元素,根据元素匹配形成第一步的res,之后我们只需要找到不在arr2中的元素放到后面即可。为了优化这个速度,我们可以直接遍历(0,max(arr1)),具体代码如下


class Solution:

    def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:
        max_num  = max(arr1)
        hashmap = {}
        for i in range(len(arr1)):
            hashmap[arr1[i]] = hashmap.get(arr1[i],0) + 1
        pre = []
        
        for c in arr2:
            if c in hashmap:
                pre.extend([c]*hashmap[c])
                hashmap[c] = 0
                
        for i in range(max_num+1):
            if i in hashmap and hashmap[i] > 0:
                pre.extend([i]*hashmap[i])

        return pre

其中,我们从hashmap中取值的时候,需要将对应的值置为0,这样可以避免重复取值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

古承风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值