LeetCode哈希表

LeetCode哈希表

LeetCode哈希表刷题记录

基础知识

哈希函数:将哈希表中元素的键值映射为元素存储位置的函数

  • 满足函数的基本特征,相同的输入一定会被映射到相同的输出 H a s h ( k e y 1 ) ≠ H a s h ( k e y 2 ) Hash(key1) \neq Hash(key2) Hash(key1)=Hash(key2) k e y 1 ≠ k e y 2 key1 \neq key2 key1=key2
  • 不同的输入也有可能被映射到相同的输出 H a s h ( k e y 1 ) = = H a s h ( k e y 2 ) Hash(key1) == Hash(key2) Hash(key1)==Hash(key2) k e y 1 ≠ k e y 2 key1 \neq key2 key1=key2 k e y 1 = = k e y 2 key1 == key2 key1==key2 都有可能
  • 得到的输出(哈希值)是固定长度的

hash

图片来源: hash fucntion

哈希表:图中的输入 e.g John Smith 叫做键(key),通过哈希函数的映射,得到值(value),存放value的数组就叫做哈希表
快速判断一个元素是否属于某个集合(快速查找)

  • 插入:输入key(John Smith)通过哈希函数得到 value = 1,就把John Smith这个输入存储在数组索引为1的区块中
  • 查找
    查找’John Smith’ 将 ‘John Smith’ 通过哈希函数得到value = 1,索引哈希表的1区块,发现存储着’John Smith’,找到了
    查找’CinderGirl’ 将’CinderGirl’ 通过哈希函数得到value = 3(假设),索引哈希表的3区块,发现存储’Sandra Dee’,找不到’CinderGirl’所以’CinderGirl’不在哈希中

常见的哈希函数:

  • 线性: H a s h ( k e y ) = k e y Hash(key) = key Hash(key)=key 或者 H a s h ( k e y ) = a × k e y + b Hash(key) = a \times key + b Hash(key)=a×key+b 适用于key为整数的情况(一般会把其他类型的数据先转化为整数),当key不连续分布且间隔较大的时候,这种方法比较浪费存储空间
  • 取余:假设哈希表的表长为 m m m, 一般取一个不大于 m m m且最接近m的质数 p p p H a s h ( k e y ) = k e y m o d    p Hash(key) = key\mod p Hash(key)=keymodp
    为什么取质数?选择质数可以尽可能的减少冲突。当key不是p的倍数时, g c d ( p , k e y ) = 1 gcd(p,key) = 1 gcd(p,key)=1(如果p为质数)根据费马小定理 a p − 1 ≡ 1 m o d    p a^{p-1} \equiv 1 \mod p ap11modp 所以 a a a的幂次在模 p p p下具有周期性,周期为 p − 1 p-1 p1,假设 k i k_i ki 是随机分布的整数,当p是质数的时候, k i m o d    p k_i \mod p kimodp的结果会在1到 p − 1 p-1 p1之间均匀分布
  • 平方取中间值:key平方扩大相近数之间的差别,然后根据哈希表长度取关键字平方值的中间几位数为哈希地址 e.g H a s h ( k e y ) = ( k e y × k e y ) / / 100 m o d    100 Hash(key) = (key \times key) //100 \mod 100 Hash(key)=(key×key)//100mod100

哈希冲突(Hash Collision)

哈希冲突(Hash Collision):不同的关键字映射到同一哈希地址,即 k e y 1 ≠ k e y 2 key1 \neq key2 key1=key2 但是 H a s h ( k e y 1 ) = = H a s h ( k e y 2 ) Hash(key1) == Hash(key2) Hash(key1)==Hash(key2)

补充:Bloom Fliter中冲突的概率计算(结合一下学校知识)

布隆过滤器:用来快速判断一个元素是否在一个集合里面的
由长度为 m m m的数组和 k k k个哈希函数组成
当一个元素被插入Bloom Filter的时候,使用 k k k个哈希函数,得到 k k k个地址,把数组对应的地址全部标记为1

以下计算Bloom Filter误判(false positive)的概率(即该元素不属于集合,但被判定属于集合的概率)

  • m m m是布隆过滤器数组的长度
  • n n n输入元素的数量
  • k k k使用的哈希函数的数量
  1. 某个哈希函数将某一位设置为 1 的概率是 1 m \frac{1}{m} m1,某个哈希函数将某一位设置为 1 的概率是 1 − 1 m 1 - \frac{1}{m} 1m1
  2. 对于一个元素,使用 k k k个哈希函数后,某一位仍然为 0 的概率是:
    ( 1 − 1 m ) k \left(1 - \frac{1}{m}\right)^k (1m1)k
  3. n n n个元素放入布隆过滤器后,某一位仍然为 0 的概率是: ( 1 − 1 m ) k n \left(1 - \frac{1}{m}\right)^{kn} (1m1)kn
  4. 某一位被设置为 1 的概率是: P { b = 1 } = 1 − ( 1 − 1 m ) k n P\{b=1\} = 1 - \left(1 - \frac{1}{m}\right)^{kn} P{b=1}=1(1m1)kn
  5. 误判的概率:误判发生在所有 k k k个哈希函数映射的位置都为 1 时$ P{\text{false positive}} = \left[1 - \left(1 - \frac{1}{m}\right){kn}\right]k ,利用泰勒展开: ,利用泰勒展开: ,利用泰勒展开:e^x \approx 1 + x 可以得到: 可以得到: 可以得到:\left(1 - \frac{1}{m}\right)^{kn} \approx \left(e{-\frac{1}{m}}\right){kn} = e^{-\frac{kn}{m}}$ 因此,误判的概率可以近似为: P { false positive } ≈ ( 1 − e − k n m ) k P\{\text{false positive}\} \approx \left(1 - e^{-\frac{kn}{m}}\right)^k P{false positive}(1emkn)k 可以用这个公式来确定需要选用多少位的布隆过滤器 m m m或者多少个哈希函数 k k k才能达到误判率低于某个值

补充:生日悖论 Birthday Paradox

只有23个人的教室,两个生日是同一天的概率大于50%

解决哈希冲突的两种方法:

开放地址

发生冲突就寻找后续的空地址,直到找到为止

H ( i ) = ( H a s h ( i ) + F ( i ) ) m o d    m H(i) = (Hash(i) + F(i)) \mod m H(i)=(Hash(i)+F(i))modm
常见的 F ( i ) = i F(i) = i F(i)=i,即发生冲突则向右移动一位,看看有没有冲突,若有空位则存进去

链地址

将具有相同哈希地址的元素存储在一个线性链表中

哈希表变存储头节点指针的数组

  • 插入关键字的时候,通过哈希函数得到链表的头指针,然后插入链表的头部(插入链表头部方便)
  • 查询关键字的时候,只需要遍历对应链表寻找元素

chaining

图片来源: geekforgeeks

数据类型实现方式是否有序元素/键是否唯一是否允许修改查询效率增删效率适用场景
列表 (List)动态数组有序元素可重复O(n)append O(1) remove O(n)存储有序数据,支持索引访问
集合 (Set)哈希表无序元素唯一O(1)O(1)去重、快速查找元素是否存在
字典 (Dict)哈希表无序 (Python 3.7+ 有序)键唯一,值可重复O(1)O(1)存储键值对,快速查找、插入、删除
defaultdict哈希表 (带默认值)无序 (Python 3.7+ 有序)键唯一,值可重复O(1)O(1)需要默认值的字典操作

题1:设计哈希集合

class MyHashSet:

    def __init__(self):
        self.size = 997 #接近1000的质数
        self.hashtable = [[] for _ in range(self.size)]
        # 二维数组,模拟数组+链表结构
    
    def hash(self, key: int) -> int:
        #定义哈希函数
        return key % self.size

    def add(self, key: int) -> None:
        hashvalue = self.hash(key)
        if key not in self.hashtable[hashvalue]:
            self.hashtable[hashvalue].append(key)

    def remove(self, key: int) -> None:
        hashvalue = self.hash(key)
        if key in self.hashtable[hashvalue]:
            self.hashtable[hashvalue].remove(key)

    def contains(self, key: int) -> bool:
        hashvalue = self.hash(key)
        if key in self.hashtable[hashvalue]:
            return True
        else:
            return False

设计哈希映射

class MyHashMap:

    def __init__(self):
        self.tablesize = 997
        self.hashtable = [[] for _ in range(self.tablesize)]
    
    def hash(self,key) -> int:
        return key % self.tablesize

    def put(self, key: int, value: int) -> None:
        hashvalue = self.hash(key)
        find = False
        for i in range(len(self.hashtable[hashvalue])):
            if key == self.hashtable[hashvalue][i][0]:
                self.hashtable[hashvalue][i][1] = value
                find = True
        if not find:
            self.hashtable[hashvalue].append([key,value])
        

    def get(self, key: int) -> int:
        hashvalue = self.hash(key)
        for i in range(len(self.hashtable[hashvalue])):
            if key == self.hashtable[hashvalue][i][0]:
                return self.hashtable[hashvalue][i][1]
        return -1
        

    def remove(self, key: int) -> None:
        hashvalue = self.hash(key)
        for i in range(len(self.hashtable[hashvalue])):
            if key == self.hashtable[hashvalue][i][0]:
                self.hashtable[hashvalue].remove(self.hashtable[hashvalue][i])
                break

题2: 检测异位词

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        alpha = [0] * 26
        for i in s:
            alpha[ord(i)-ord('a')] += 1
        for i in t:
            alpha[ord(i)-ord('a')] -= 1
        for i in range(26):
            if alpha[i] != 0:
                return False
        return True

升级版:判断ransomNote字符串是否可以用magazine里面的字母构成

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        alpha = [0] * 26
        for letter in magazine:
            alpha[ord(letter) - ord('a')] += 1
        for letter in ransomNote:
            if alpha[ord(letter) - ord('a')] == 0:
                return False
            else:
                alpha[ord(letter) - ord('a')] -= 1
        return True

题3: 返回集合交集

由于数据的最大值 ≤ 1000 \leq 1000 1000这道题目可以继续用数组作为哈希表,但是如果数据的值很大的话,用数组会造成很大的存储空间浪费,所以可以考虑dict字典结构来作哈希表

class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        intersect = []
        h = {}
        for i in nums1:
            if i not in h:
                h[i] = 1
        for i in nums2:
            if i in h:
                intersect.append(i)
                del h[i]  #清空键值对,以免重复添加
        return intersect

还可以用类似合并数组的思想,排序后再求交集

class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        intersect = []
        nums1.sort()
        nums2.sort()
        ptr1 = 0
        ptr2 = 0
        while ptr1 < len(nums1) and ptr2 < len(nums2):
            if nums1[ptr1] == nums2[ptr2]:
                intersect.append(nums1[ptr1])
                ptr1 += 1
                ptr2 += 1
                while ptr1 < len(nums1) and nums1[ptr1] == nums1[ptr1-1]: # 去重
                    ptr1 += 1
            elif nums1[ptr1] < nums2[ptr2]:
                ptr1 += 1
            else:
                ptr2 += 1
        return intersect

进阶:返回时考虑出现次数,以出现次数少的为准

初写:

class Solution:
    def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
        dict1 = {}
        dict2 = {}
        result = []
        for i in nums1:
            if i in dict1:
                dict1[i] += 1
            else:
                dict1[i] = 1
        for i in nums2:
            if i in dict2:
                dict2[i] += 1
            else:
                dict2[i] = 1
        for key in dict1:
            if key in dict2:
                for _ in range(min(dict1[key],dict2[key])):
                    result.append(key)
        return result

优化,完全不需要遍历两次,第一次dict1记录了元素在iist1中出现的次数,遍历dict2的时候只需要逐步减掉次数就行,当dict1对应key的次数被减到0的时候,把它从字典里删除

class Solution:
   def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
       dict1 = {}
       result = []
       for i in nums1:
           if i in dict1:
               dict1[i] += 1
           else:
               dict1[i] = 1
       for i in nums2:
           if i in dict1:
               dict1[i] -= 1
               result.append(i)
               if dict1[i] == 0:
                   del dict1[i]
       return result

题4:寻找快乐数(每位平方和多次相加最终能为1的数)

class Solution:
    def isHappy(self, n: int) -> bool:
        list1 = []
        nex = n
        list1.append(n)
        while nex != 1:
            string = str(nex)
            count = 0
            for i in string:
                count += int(i) * int(i)
            nex = count
            if nex in list1:
                return False
            else:
                list1.append(nex)
        return True

算各位数字的和用转换成string来算,虽然方便了写程序,但实际上算法的时间复杂度变高 O ( l o g n ) − > O ( n ) O(logn) -> O(n) O(logn)>O(n)

class Solution:
    def getNext(self, n: int) -> int:
        total_sum = 0
        while n > 0:
            n, digit = divmod(n, 10)  
            total_sum += digit ** 2   
        return total_sum

    def isHappy(self, n: int) -> bool:
        num_set = set() 
        while n != 1 and n not in num_set:
            num_set.add(n) ]
            n = self.getNext(n)  
        return n == 1 

如果直接进行数学计算的话,算 total_sum 的循环只需要循环 log ⁡ 10 n \log_{10} n log10n 次,判断是否为快乐数的循环,循环次数随着n的数位减小而减小

本思路使用数据是否重复来检验是否进入无限循环

其实结合一下昨天的链表判断有无环,用快慢指针的想法也行,都能检测是否进入无限循环

题5:两数之和为target

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        dic = {}
        for idx in range(len(nums)):
            if nums[idx] in dic:
                return [idx, dic[nums[idx]]]
            else:
                dic[target - nums[idx]] = idx

题6:找出是否存在满足差值小于valueDiff下标索引差值小于indexDiff的一对(i,j)

一道非常综合的难题,考察了滑动窗口 二分查找

思路分析:因为哈希表只能记录一个条件,不能同时记录两个条件,我一开始想着把 [ n u m s [ i ] − v a l u e D i f f , n u m s [ i ] + v a l u e D i f f ] [nums[i]-valueDiff, nums[i]+valueDiff] [nums[i]valueDiff,nums[i]+valueDiff]全部都存到哈希表(dict)中,这样下次检索,直接判断索引差值是否符合条件

class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
        dic = {}
        for i in range(len(nums)):
            if nums[i] in dic:
                if abs(dic[nums[i]] - i) <= indexDiff:
                    return True
                else:
                    for j in range(nums[i]-valueDiff, nums[i]+valueDiff+1):
                        dic[j] = i
            else:
                for j in range(nums[i]-valueDiff, nums[i]+valueDiff+1):
                    dic[j] = i #即使这个值被保存过了,也存新的index i,因为i比较小
        return False

但是这样超出内存限制了,想着要优化,不要在dic里面存那么多东西,只要是index超过indexDiff的,全部可以从dict里面去除,但是用dict结构很难实现这一点,所以想到用left和right双指针的滑动窗口来保证index的差值,然后对窗口进行排序(因为在窗口里的数,都满足indexDiff,排序之后index乱套了也无所谓),使用二分查找找和right最接近的值,判断是否满足ValueDiff

class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
        from bisect import bisect_left # 调用函数实现二分查找
        from sortedcontainers import SortedList 
        window = SortedList() #维护一个长度不超过indexDiff的窗口,这个窗口是有序的
        left = 0
        right = 0
        while right < len(nums):
            window.add(nums[right])
            if right - left  > indexDiff: 
                #没有+1哦,right left本身就是索引
                window.remove(nums[left])
                left += 1
            idx = bisect_left(window,nums[right])
            if idx > 0 and abs(window[idx-1] - nums[right]) <= valueDiff:
                return True
            if idx + 1 < right - left + 1 and abs(window[idx+1] - nums[right]) <= valueDiff:
                #可以优化成 idx < right - left只是为了自己看起来更清楚
                return True
            right += 1
        return False

题7:四数相加 从四个数组里面找nums1[i] + nums2[j] + nums3[k] + nums4[l] = 0的(i,j,k,l)总共有多少对

本题的关键在于想着如何去优化,四个for循环的暴力解 O ( n 4 ) O(n^4) O(n4)的时间复杂度,因为涉及到查找问题,其实与检测异位词还有两数之和为target是一样的,只不过之前是两数之和/处理两个数组,现在有四个数组,所以一对对处理,把复杂度降低到 O ( n 2 ) O(n^2) O(n2)

class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        dic = {}
        count = 0
        for i in nums1:
            for j in nums2:
                if i + j in dic:
                    dic[i+j] += 1
                else:
                    dic[i+j] = 1
        for i in nums3:
            for j in nums4:
                if -i - j in dic:
                    count += dic[-i-j]
        return count

题8:三数之和为0,返回[[nums[i], nums[j], nums[k]]需要去重

这题需要回看复习 第一遍不能自己构建一个可以跑通的思路,难点主要在去重

方法一:哈希表

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result = []
        for i in range(len(nums) - 2):
            if nums[i] > 0:
                # 最小的第一个数大于0,一定不存在满足要求的三元组
                return result
            if i > 0 and nums[i] == nums[i-1]:
                continue 
                # 去重,如果开头为nums[i]已经在之前遍历过了,就可以直接跳过
            table = set() #记录需要的数
            k = i+1
            while k < len(nums):
                #寻找k,三元组中的第三个元素
                if -nums[k] - nums[i] in table:
                    #存在
                    result.append([nums[i], -nums[k]-nums[i], nums[k]])
                    while k < len(nums) - 1 and nums[k] == nums[k+1]:
                        k += 1
                table.add(nums[k])
                k += 1
        return result

注意:

  • 第二重循环遍历的是k还是j 如果遍历的是j那么k还需要单独一层循环来处理,不能降低算法时间复杂度,而且很难去重,所以用k作为第二层循环的变量,j卡在ki中间,可以把那些出现过的k作为j的备选项,加入set
  • if i > 0 and nums[i] == nums[i-1] continue条件 去重i是当nums[i]这个数值已经尝试过作为三元组的开头了,就不需要再尝试一遍,所以是和前一个比较,如果和后一个比较,就会导致默认第二个数nums[j]不能等于第一个数nums[i],会漏掉[-1, -1, 2]这种情况
  • k的初始值i+1 虽然k代表第三个元素,但是已经遍历过的k要作为j的后背选项加入table所以还是要从i+1开始避免遗漏
  • while k < len(nums) - 1 and nums[k] == nums[k+1]:循环是在if语句里面还是外面如果放在if外面,相当于逻辑上默认 j ≠ k j \neq k j=k,因为同样的元素只会被add进入table一次,之后k变成k+1不同于被add进入table的元素,处理[-2,1,1]会有问题

方法二:滑动窗口

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result = []
        for i in range(len(nums) - 2):
            # i为三元组的第一个元素,从头遍历到尾部
            if nums[i] > 0:
                return result
            if i > 0 and nums[i] == nums[i-1]:
                continue
                # 去重
            left = i + 1
            right = len(nums) - 1
            # left和right分别代表j和k
            while left < right:
                if nums[i] + nums[left] + nums[right] == 0:
                    result.append([nums[i], nums[left], nums[right]])
                    #去重
                    while nums[left] == nums[left+1] and left + 1 < right:
                        left += 1
                    while nums[right] == nums[right-1] and right - 1 > left:
                        right -= 1
                    left += 1
                    right -= 1
                elif nums[i] + nums[left] + nums[right] > 0:
                    right -= 1
                else:
                    left += 1
        return result

注意:

  • 双指针的设计 leftright初始化为i + 1len(nums) - 1而不是i + 1i + 2是为了方便移动,对于已经排序的数组,如果right = i + 2,就很难设计当nums[i] + nums[left] + nums[right] < 0时指针如何移动,是移动left还是right,如果移动right那么什么时候移动left,反之亦然,如果同时移动,相当于限制死了left right永远相邻。如果用一头一尾的设计,就刚好能处理大于小于两种情况
  • 什么时候while nums[left] == nums[left+1] and left + 1 < right去重:一定是在找到一组后,不然相当于限制了 n u m s [ l e f t ] ≠ n u m s [ r i g h t ] nums[left] \neq nums[right] nums[left]=nums[right] 如果非要把去重逻辑放外面写,可以类比i的去重,if i > 0 and nums[i] == nums[i-1]:,程序如下
  • 去重完别忘了left += 1 right -= 1更新值
  • 若题干要求返回下标索引的话,就无法用双指针法,因为双指针法要排序,排序之后索引失效啦
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result = []
        for i in range(len(nums) - 2):
            # i为三元组的第一个元素,从头遍历到尾部
            if nums[i] > 0:
                return result
            if i > 0 and nums[i] == nums[i-1]:
                continue
                # 去重
            left = i + 1
            right = len(nums) - 1
            # left和right分别代表j和k
            while left < right:
                while left > i + 1 and nums[left] == nums[left-1] and left  < right:
                    left += 1
                while right < len(nums) - 1 and nums[right] == nums[right + 1] and left < right:
                    right -= 1
                if left < right:
                    if nums[i] + nums[left] + nums[right] == 0:
                        result.append([nums[i], nums[left], nums[right]])
                        left += 1
                        right -= 1
                    elif nums[i] + nums[left] + nums[right] > 0:
                        right -= 1
                    else:
                        left += 1
        return result

题9:四数之和为target,四个数来自于同一个数组,不能重复

和三数之和用同样的方式降低时间复杂的

  1. 排序,方便进行双指针遍历
  2. 双指针遍历减少一重循环
class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        result = []
        for i in range(len(nums)-3):
            # 可以尝试做剪枝操作,但是要注意条件
            # 不是 
            # if nums[i] > target:
            #     break
            # 因为target可以是负数
            if nums[i] > target and nums[i] > 0:
                # 去重
                break
            if i > 0 and nums[i] == nums[i-1]:
                continue
            for j in range(i+1, len(nums)-2):
                if nums[i] + nums[j] > target and nums[j] > 0:
                    break
                    # 如果nums[j] > 0 的话后面的数一定大于0,只要是大于0的数,越加越大
                if j > i + 1 and nums[j] == nums[j-1]:
                    # 去重
                    continue
                left = j + 1
                right = len(nums) - 1
                # 双指针遍历,思路同三数之和
                while left < right:
                    if nums[i] + nums[j] + nums[left] + nums[right] == target:
                        result.append([nums[i], nums[j], nums[left], nums[right]])
                        # 去重
                        while left < right  and nums[left] == nums[left+1]:
                            left += 1
                        while right > left and nums[right] == nums[right-1]:
                            right -= 1
                        left += 1
                        right -=1
                    elif nums[i] + nums[j] + nums[left] + nums[right] < target:
                        left += 1
                    else:
                        right -= 1
        return result

让我思考的点:

  • left += 1 right += 1放在去重前还是后:如果放在去重前,可能会因为left < right而推出循环,但实际上此时去重还没有去干净,而如果放在去重后,如果因为left < right而退出循环,那么加1过后,在外层循环判断时,一定能正确退出

总结

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值