数据结构|LeetCode(力扣)经典题:哈希表

哈希表

更多请查看我的专栏:LeetCode(力扣)刷题指南

可直接在LeetCode中搜索题目名称

文章目录

    • 哈希表
  • 1. 两数之和
    • 1.1 解决方案
      • 1. 暴力法(时间O(n^2):5012 ms)
      • 2.暴力法进阶:时间复杂度O(n):940 ms
      • 3.哈希法(时间复杂度O(n):52 ms)
      • 4.哈希法进阶版(时间复杂度O(n):52 ms)
  • 2. 有效的字母异位词
    • 2.1 解决办法
      • 1.排序(O(nlogn):60 ms)
      • 2.哈希表(时间O(n))
    • 2.2 思考与总结
      • 1.set()
  • 3. 多数元素(分治算法、Boyer-Moore 投票算法)
    • 3.1 解决方案
      • 1.方法一:哈希表
      • 2.方法二:排序
      • 3.方法三:随机化
      • 4.方法四:分治算法
      • 5.方法五:Boyer-Moore 投票算法
    • 3.2 思考与总结
      • 1.哈希表
      • 2.分治算法
      • 3.Boyer-Moore投票算法
  • 4.拼写单词(字符串)
    • 4.1 解决方案
      • **哈希表计数**
    • 4.2 思考和总结
        • 1.if-else的语言问题
      • 2.Counter的性质
      • 3. 深复制copy()

1. 两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

1.1 解决方案

1. 暴力法(时间O(n^2):5012 ms)

  1. 两次遍历,第一次遍历每个元素,设x;
  2. 第二次遍历,查找是否有与target-x相等的目标元素。
class Solution:
    def twoSum(self,nums: List[int], target: int) -> List[int]:
        for i in range(len(nums)):
            b = target - nums[i]
            for j in range(len(nums)):
                if nums[j]==b and i!=j:
                    return [i,j]

2.暴力法进阶:时间复杂度O(n):940 ms

  1. 一次遍历,遍历每个元素,设x;
  2. 通过切片复制nums数组x后面的数组nums1;
  3. 通过list.index查找y=target-x在 nums1 中的位置。
class Solution:
    def twoSum(self,nums: list[int], target: int) -> list[int]:
        for i in range(len(nums)):
            a=target-nums[i]
            nums1=nums[i+1:]
            if a in nums1:
                return [i,nums1.index(a)+i+1]
        return 0

​ 第二个元素的查找并不需要再从头来一遍遍历,只需要从第一个元素之间或之后查找即可。但为了方便index 这里选择从第一个元素之前查找。


3.哈希法(时间复杂度O(n):52 ms)

  1. 通过enumerate()建立一个字典,模仿哈希表;
  2. 然后根据target-num在字典中查找键,并返回相应的数组位置。
class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        hash={
     }
        for index1,num1 in enumerate(nums):
            hash[num1] = index1 #字典的键是数组nums的元素,值是元素序号
        for index,num in enumerate(nums):
            i= hash.get(target-num)
            if i is not None and i!=index:
                return [i,index]

​ 为了对运行时间复杂度进行优化,我们需要一种更有效的方法来检查数组中是否存在目标元素。如果存在,我们需要找出它的索引。保持数组中的每个元素与其索引相互对应的最好方法是什么?哈希表。

4.哈希法进阶版(时间复杂度O(n):52 ms)

  1. 通过enumerate()建立一个字典,模仿哈希表;
  2. 在建立字典的同时,查找字典中是否有与target-num相对应的键
class Solution:
    def twoSum(self,nums: List[int], target: int) -> List[int]:
        map = {
     }
        for index, num in enumerate(nums):
            if (map.get(target - num) is not None )&(index!=map.get(target - num)) :
                return [index, map.get(target - num)]
            map[num] = index

​ 类似于暴力法版本,不需要在整个字典中查找,可以在第一个元素之间的字典中查找,因此就只需要一次循环可解决。但效果并不明显。

2. 有效的字母异位词

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

示例 1:

输入: s = “anagram”, t = “nagaram”
输出: true


示例 2:

输入: s = “rat”, t = “car”
输出: false


说明:
你可以假设字符串只包含小写字母。

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

2.1 解决办法

1.排序(O(nlogn):60 ms)

通过将 s 的字母重新排列成 t 来生成变位词。因此,如果 T 是 S 的变位词,对两个字符串进行排序将产生两个相同的字符串。此外,如果 s 和 t 的长度不同,t 不能是 s 的变位词,我们可以提前返回。

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        return sorted(s)==sorted(t)

复杂度分析:

时间复杂度:O(nlogn),假设 n 是 ss 的长度,排序成本O(nlogn) 和比较两个字符串的成本 O(n)。排序时间占主导地位,总体时间复杂度为 O(nlogn)。
空间复杂度:空间取决于排序实现,这依赖于语言的细节,和函数的设计方式。例如,可以将函数参数类型更改为 char[]。

2.哈希表(时间O(n))

一个重要的前提“假设两个字符串只包含小写字母”,小写字母一共也就 26 个,因此:

  1. 可以利用两个长度都为 26 的字符数组来统计每个字符串中小写字母出现的次数,然后再对比是否相等;

  2. 以只利用一个长度为 26 的字符数组,将出现在字符串 s 里的字符个数加 1,而出现在字符串 t 里的字符个数减 1,最后判断每个小写字母的个数是否都为 0。

  3. 或者我们可以先用计数器表计算 s,然后用 t 减少计数器表中的每个字母的计数器。如果在任何时候计数器低于零,我们知道 t 包含一个不在 s 中的额外字母,并立即返回 FALSE。

按上述操作,可得出结论:s 和 t 互为字母异位词。

class Solution(object):
    def isAnagram(self, s, t):
        """
        :type s: str
        :type t: str
        :rtype: bool
        """
        if len(s) != len(t):
            return False
        dicts = collections.defaultdict(int)  #默认字典!!!!!
        for i in range(len(s)):
            dicts[s[i]] = dicts[s[i]] + 1
            dicts[t[i]] = dicts[t[i]] - 1
        for val in dicts.values():
            if val != 0:
                return False
        return True

####哈希的另外一种写法
class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        
        d = {
     }
        for c in s:
            d[c] = d.get(c, 0) + 1
            
        for c in t:
            d[c] = d.get(c, 0) - 1
            if d[c] < 0:
                return False
    
        return True

复杂度分析

时间复杂度:O(n),因为访问计数器表是一个固定的时间操作。

空间复杂度:O(1)。尽管我们使用了额外的空间,但是空间的复杂性是 O(1),因为无论 N有多大,表的大小都保持不变。


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

解答
使用哈希表而不是固定大小的计数器。想象一下,分配一个大的数组来适应整个 Unicode 字符范围,这个范围可能超过 100万。哈希表是一种更通用的解决方案,可以适应任何字符范围。

2.2 思考与总结

应该先考虑字符串 s 和 t 是否长度相等,如果不同,可直接返回False。

1.set()

Python利用了 set 的内置优化和特性减少运算,以及使用布尔运算提高效率。

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t): return False
        se = set(s)
        if se == set(t):
            for i in se:
                # 直接比较字符元素个数比较字符的个数
                if s.count(i) != t.count(i):return False
            return True
        else:
            return False

3. 多数元素(分治算法、Boyer-Moore 投票算法)

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [3,2,3]
输出: 3

示例 2:

输入: [2,2,1,1,1,2,2]
输出: 2

3.1 解决方案

[n/2]表示向下取整

1.方法一:哈希表

​ 我们知道出现次数最多的元素大于 [n/2] 次,所以可以用哈希表来快速统计每个元素出现的次数。

算法思路

​ 我们使用哈希映射(HashMap)来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素,值表示该元素出现的次数。

​ 我们用一个循环遍历数组 nums 并将数组中的每个元素加入哈希映射中。在这之后,我们遍历哈希映射中的所有键值对,返回值最大的键**。我们同样也可以在遍历数组 nums 时候使用打擂台的方法,维护最大的值,这样省去了最后对哈希映射的遍历**。

class Solution:
    def majorityElement(self, nums):
        counts = collections.Counter(nums)  #创建一个可计数的字典
        return max(counts.keys(), key=counts.get)  

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。我们遍历数组 nums 一次,对于 nums 中的每一个元素,将其插入哈希表都只需要常数时间。如果在遍历时没有维护最大值,在遍历结束后还需要对哈希表进行遍历,因为哈希表中占用的空间为 O(n)(可参考下文的空间复杂度分析),那么遍历的时间不会超过 O(n)。因此总时间复杂度为 O(n)。
  • 空间复杂度:O(n)。哈希表最多包含 n-[n/2] 个键值对,所以占用的空间为 O(n)。这是因为任意一个长度为 n的数组最多只能包含 n个不同的值,但题中保证 nums 一定有一个众数,会占用(最少)n/2个数字。因此最多有 n-[n/2] 个不同的元素。

2.方法二:排序

​ 如果将数组 nums 中的所有元素按照单调递增或单调递减的顺序排序,那么下标为 n/2的元素(下标从 0 开始)一定是众数。

算法思路

​ 对于这种算法,我们先将 nums 数组排序,然后返回上文所说的下标对应的元素。下面的图中解释了为什么这种策略是有效的。在下图中,第一个例子是 n为奇数的情况,第二个例子是 n为偶数的情况。

​ 对于每种情况,数组下面的线表示如果众数是数组中的最小值时覆盖的下标,数组上面的线表示如果众数是数组中的最大值时覆盖的下标。对于其他的情况,这条线会在这两种极端情况的中间。对于这两种极端情况,它们会在下标为 [n/2] 的地方有重叠。因此,无论众数是多少,返回 [n/2] 下标对应的值都是正确的。

class Solution:
    def majorityElement(self, nums):
        nums.sort()
        return nums[len(nums)//2]

复杂度分析

  • 时间复杂度:O(nlog n)。将数组排序的时间复杂度为 O(nlog n)
  • 空间复杂度:O(log n)。如果使用语言自带的排序算法,需要使用 O(log n)的栈空间。如果自己编写堆排序,则只需要使用 O(1)的额外空间。

3.方法三:随机化

​ 因为超过[n/2] 的数组下标被众数占据了,这样我们随机挑选一个下标对应的元素并验证,有很大的概率能找到众数。

算法思路

​ 由于一个给定的下标对应的数字很有可能是众数,我们随机挑选一个下标,检查它是否是众数,如果是就返回,否则继续随机挑选。

import random

class Solution:
    def majorityElement(self, nums):
        majority_count = len(nums)//2   #初始先找下标为[n/2]的元素
        while True:
            candidate = random.choice(nums)
            if sum(1 for elem in nums if elem == candidate) > majority_count:
                return candidate

复杂度分析

  • 时间复杂度:理论上最坏情况下的时间复杂度为 O( ∞ \infty ),因为如果我们的运气很差,这个算法会一直找不到众数,随机挑选无穷多次,所以最坏时间复杂度是没有上限的。然而,运行的期望时间是线性的。为了更简单地分析,先说服你自己:由于众数占据 超过 数组一半的位置,期望的随机次数会小于众数占据数组恰好一半的情况。因此,我们可以计算随机的期望次数(下标为 prob 为原问题,mod 为众数恰好占据数组一半数目的问题):

    计算方法为:当众数恰好占据数组的一半时,第一次随机我们有 1/2 的概率找到众数,如果没有找到,则第二次随机时,包含上一次我们有1/4 的概率找到众数,以此类推。因此期望的次数为 i *1/2i 的和,可以计算出这个和为 2,说明期望的随机次数是常数。每一次随机后,我们需要 O(n)的时间判断这个数是否为众数,因此期望的时间复杂度为 O(n)。

  • 空间复杂度:O(1)。随机方法只需要常数级别的额外空间。

4.方法四:分治算法

​ 如果数 a 是数组 nums 的众数,如果我们将 nums 分成两部分,那么 a 必定是至少一部分的众数。

我们可以使用反证法来证明这个结论。假设 a 既不是左半部分的众数,也不是右半部分的众数,那么 a 出现的次数少于 l / 2 + r / 2 次,其中 lr 分别是左半部分和右半部分的长度。由于 l / 2 + r / 2 <= (l + r) / 2,说明 a 也不是数组 nums 的众数,因此出现了矛盾。所以这个结论是正确的。

这样以来,我们就可以使用分治法解决这个问题:将数组分成左右两部分,分别求出左半部分的众数 a1 以及右半部分的众数 a2,随后在 a1a2 中选出正确的众数。

算法思路

​ 我们使用经典的分治算法递归求解,直到所有的子问题都是长度为 1 的数组:

  • 长度为 1 的子数组中唯一的数显然是众数,直接返回即可。
  • 如果回溯后某区间的长度大于 1,我们必须将左右子区间的值合并。
  1. 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
  2. 否则,我们需要比较两个众数在整个区间内出现的次数来决定该区间的众数。
class Solution:
    def majorityElement(self, nums, lo=0, hi=None):
        def majority_element_rec(lo, hi):

            #基本情况:只有一个元素的子数组
            if lo == hi:
                return nums[lo]

            #递归左右子数组
            mid = (hi-lo)//2 + lo
            left = majority_element_rec(lo, mid)
            right = majority_element_rec(mid+1, hi)

            # 如果左右子数组的众数相等,则返回该数组的众数
            if left == right:
                return left

            # 否则,分别计算左右子数组众数的个数,返回更大的那个为该数组的众数。
            left_count = sum(1 for i in range(lo, hi+1) if nums[i] == left)
            right_count = sum(1 for i in range(lo, hi+1) if nums[i] == right)

            return left if left_count > right_count else right

        return majority_element_rec(0, len(nums)-1)

复杂度分析

  • 时间复杂度:O(nlog n)。函数 majority_element_rec() 会求解 2 个长度为n/2 的子问题,并做两遍长度为 n 的线性扫描(计算left_countright_count)。因此,分治算法的时间复杂度可以表示为: T ( n ) = 2 T ( n 2 ) + 2 n T(n) = 2T(\frac{n}{2}) + 2n T(n)=2T(2n)+2n

    根据 主定理,本题满足第二种情况,所以时间复杂度可以表示为:

    数据结构|LeetCode(力扣)经典题:哈希表_第1张图片

  • 空间复杂度:O(log n)。尽管分治算法没有直接分配额外的数组空间,但在递归的过程中使用了额外的栈空间。算法每次将数组从中间分成两部分,所以数组长度变为 1 之前需要进行 O(log n)次递归,即空间复杂度为 O(log n)。

5.方法五:Boyer-Moore 投票算法

​ 如果我们把众数记为 +1,把其他数记为 -1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多。

​ Boyer-Moore 算法的本质和方法四中的分治十分类似。我们首先给出 Boyer-Moore 算法的详细步骤:

  • 我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count0
  • 我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x
    • 如果 xcandidate 相等,那么计数器 count 的值增加 1
    • 如果 xcandidate 不等,那么计数器 count 的值减少 1
  • 在遍历完成后,candidate 即为整个数组的众数。

我们举一个具体的例子,例如下面的这个数组:

[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]

在遍历到数组中的第一个元素以及每个在 | 之后的元素时,candidate 都会因为 count 的值变为 0 而发生改变。最后一次 candidate 的值从 5 变为 7,也就是这个数组中的众数。


Boyer-Moore 算法的正确性较难证明,这里给出一种较为详细的用例子辅助证明的思路,供读者参考:

首先我们根据算法步骤中对 count 的定义,可以发现:在对整个数组进行遍历的过程中,count 的值一定非负。这是因为如果 count 的值为 0,那么在这一轮遍历的开始时刻,我们会将 x 的值赋予 candidate 并在接下来的一步中将 count 的值增加 1。因此 count 的值在遍历的过程中一直保持非负。

那么 count 本身除了计数器之外,还有什么更深层次的意义呢?我们还是以数组

[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]

作为例子,首先写下它在每一步遍历时 candidatecount 的值:

nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
candidate:  7  7  7  7  7  7   5  5   5  5  5  5   7  7  7  7
count:      1  2  1  2  1  0   1  0   1  2  1  0   1  2  3  4

我们再定义一个变量 value,它和真正的众数 maj 绑定。在每一步遍历时,如果当前的数 xmaj 相等,那么 value 的值加 1,否则减 1value 的实际意义即为:到当前的这一步遍历为止,众数出现的次数比非众数多出了多少次。我们将 value 的值也写在下方:

nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
value:      1  2  1  2  1  0  -1  0  -1 -2 -1  0   1  2  3  4

有没有发现什么?我们将 countvalue 放在一起:

nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
count:      1  2  1  2  1  0   1  0   1  2  1  0   1  2  3  4
value:      1  2  1  2  1  0  -1  0  -1 -2 -1  0   1  2  3  4

发现在每一步遍历中,countvalue 要么相等,要么互为相反数!并且在候选众数 candidate 就是 maj 时,它们相等,candidate 是其它的数时,它们互为相反数!

​ 为什么会有这么奇妙的性质呢?这并不难证明:我们将候选众数 candidate 保持不变的连续的遍历称为「一段」。在同一段中,count 的值是根据 candidate == x 的判断进行加减的。那么如果 candidate 恰好为 maj,那么在这一段中,countvalue 的变化是同步的;如果 candidate 不为 maj,那么在这一段中 countvalue 的变化是相反的。因此就有了这样一个奇妙的性质。

这样以来,由于:

  • 我们证明了 count 的值一直为非负,在最后一步遍历结束后也是如此;
  • 由于 value 的值与真正的众数 maj 绑定,并且它表示「众数出现的次数比非众数多出了多少次」,那么在最后一步遍历结束后,value 的值为正数;

在最后一步遍历结束后,count 非负,value 为正数,所以它们不可能互为相反数,只可能相等,即 count == value。因此在最后「一段」中,countvalue 的变化是同步的,也就是说,candidate 中存储的候选众数就是真正的众数 maj

class Solution:
    def majorityElement(self, nums):
        count = 0
        candidate = None

        for num in nums:
            if count == 0:
                candidate = num
            count += (1 if num == candidate else -1)

        return candidate

复杂度分析

  • 时间复杂度:O(n)。Boyer-Moore 算法只对数组进行了一次遍历。
  • 空间复杂度:O(1)。Boyer-Moore 算法只需要常数级别的额外空间。

3.2 思考与总结

1.哈希表

自己的代码:

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        count={
     }
        for i in nums:
            if i in count.keys():
                count[i]+=1
            else:
                count[i]=1
        for i,j in count.items():
            if j==max(count.values()):
                return i

2.分治算法

分治法中如果两个区间的众数不同但是出现次数相同怎么办?

​ 因为数组的众数一定,至少与左右子数组的众数中的一个相等。假设数组的众数只与左子数组的众数相等,则它的数量一定多于右子数组的众数数量;反之亦然。因此,若出现两个区间的众数不同且出现次数相同,则这这个子数组的众数一定不是对的,不影响算法递归的最终结果。

3.Boyer-Moore投票算法

算法思路:因为众数大于n/2,所以唯有当candidate=众数时,遍历完数组,一定大于0。

4.拼写单词(字符串)

给你一份『词汇表』(字符串数组) words 和一张『字母表』(字符串) chars

假如你可以用 chars 中的『字母』(字符)拼写出 words 中的某个『单词』(字符串),那么我们就认为你掌握了这个单词。

注意:每次拼写时,chars 中的每个字母都只能用一次。

返回词汇表 words 中你掌握的所有单词的 长度之和

示例 1:

输入:words = ["cat","bt","hat","tree"], chars = "atach"
输出:6
解释: 
可以形成字符串 "cat" 和 "hat",所以答案是 3 + 3 = 6。

示例 2:

输入:words = ["hello","world","leetcode"], chars = "welldonehoneyr"
输出:10
解释:
可以形成字符串 "hello" 和 "world",所以答案是 5 + 5 = 10。

提示:

  1. 1 <= words.length <= 1000
  2. 1 <= words[i].length, chars.length <= 100
  3. 所有字符串中都仅包含小写英文字母

4.1 解决方案

哈希表计数

算法思路

​ 显然,对于一个单词 word,只要其中的每个字母的数量都不大于 chars 中对应的字母的数量,那么就可以用 chars 中的字母拼写出 word

​ 所以我们只需要用一个哈希表存储 chars 中每个字母的数量,再用一个哈希表存储 word 中每个字母的数量,最后将这两个哈希表的键值对逐一进行比较即可。

版本一:

数据结构|LeetCode(力扣)经典题:哈希表_第2张图片

class Solution:
    def countCharacters(self, words: List[str], chars: str) -> int:
        chars_cnt = collections.Counter(chars) #生成一个哈希表,且对出现在chars的key出现次数进行计数
      
        ans = 0
        for word in words:
            word_cnt = collections.Counter(word)
            for c in word_cnt:
               
               #这里注意:对于Counter对象chars_cnt、word_cnt:若c in word_cnt,但 c not in chars_cnt,此时chars_cnt[c]=0
                if chars_cnt[c] < word_cnt[c]:  #即时两个Counter对象的键
                    break
            else:   #注意!else子句与第二层for同个缩进级别
                ans += len(word)
        return ans

版本二:

#上个版本使用了两个for,太繁琐
from collections import Counter
class Solution:
    def countCharacters(self, words: List[str], chars: str) -> int:
        c = Counter(chars)
        res = 0
        for word in words:
            word_c = Counter(word)
            if not (word_c - c):
                res += len(word)
        return res
#====================================================================#
#====================================================================# 
>>> from collections import Counter
>>> a='abdtaasdf'
>>> b='asdfxc'
>>> a1=Counter(a)
>>> a1
Counter({
     'a': 3, 'd': 2, 'b': 1, 't': 1, 's': 1, 'f': 1})
>>> b1=Counter(b)
>>> b1
Counter({
     'a': 1, 's': 1, 'd': 1, 'f': 1, 'x': 1, 'c': 1})
>>> b1-a1   #两个Counter相减:b1-a1——for c in a:if c in b :b1删除c的计数
Counter({
     'x': 1, 'c': 1})

版本三:

#pythonic
class Solution:
    def countCharacters(self, words: List[str], chars: str) -> int:
        ans = 0
        cnt = collections.Counter(chars)
        for w in words:
            c = collections.Counter(w)
            if all([c[i] <= cnt[i] for i in c]): #all()的秒用
                ans += len(w)
        return ans

复杂度分析

  • 时间复杂度:O(n),其中 n 为所有字符串的长度和。我们需要遍历每个字符串,包括 chars 以及数组 words 中的每个单词。
  • 空间复杂度:O(S),其中 S 为字符集大小,在本题中 S 的值为 26(所有字符串仅包含小写字母)。程序运行过程中,最多同时存在两个哈希表,使用的空间均不超过字符集大小 S,因此空间复杂度为 O(S)。

4.2 思考和总结

1.if-else的语言问题

​ 注意第11行的else子句的缩进级别

class Solution:
    def countCharacters(self, words: List[str], chars: str) -> int:
        chars_cnt = collections.Counter(chars) #生成一个哈希表,且对出现在chars的key出现次数进行计数
      
        ans = 0
        for word in words:
            word_cnt = collections.Counter(word)
            for c in word_cnt:
                if chars_cnt[c] < word_cnt[c]:  #即时两个字典的键
                    break
            else:   #注意!else子句与第二层for同个缩进级别
                ans += len(word)
        return ans

2.Counter的性质

Counter是一个dict子类,主要是用来对你访问的对象的频率进行计数。

subtract([iterable-or-mapping]):从迭代对象中减去元素,输入输出可以是0或者负数

# 减少对象,或者使用c.subtract(d)
>>> c - d
Counter({
     'hello': 2, 'world': 1, 'nihao': 1})
class Solution:
    def countCharacters(self, words: List[str], chars: str) -> int:
        chars_cnt = collections.Counter(chars) #生成一个哈希表,且对出现在chars的key出现次数进行计数
      
        ans = 0
        for word in words:
            word_cnt = collections.Counter(word)
            for c in word_cnt:  #这里c是指word中的单个字符
               
                #这里注意:对于Counter对象chars_cnt、word_cnt:若c in word_cnt,但 c not in chars_cnt,此时chars_cnt[c]=0
                if chars_cnt[c] < word_cnt[c]:  
                    break
            else:   #注意!else子句与第二层for同个缩进级别
                ans += len(word)
        return ans
#====================================================================#
#====================================================================# 
#上个版本使用了两个for,太繁琐
from collections import Counter
class Solution:
    def countCharacters(self, words: List[str], chars: str) -> int:
        c = Counter(chars)
        res = 0
        for word in words:
            word_c = Counter(word)
            if not (word_c - c):  #注意!这里是两个Counter对象相减,下面是测试
                res += len(word)
        return res
#====================================================================#
#====================================================================# 
#对Counter对象相减的测试
>>> from collections import Counter
>>> a='abdtaasdf'
>>> b='asdfxc'
>>> a1=Counter(a)
>>> a1
Counter({
     'a': 3, 's': 1, 'd': 2, 'f': 1, 'b': 1, 't': 1})
>>> b1=Counter(b)
>>> b1
Counter({
     'a': 1, 's': 1, 'd': 1, 'f': 1, 'x': 1, 'c': 1})
>>> b1-a1   #两个Counter相减:b1-a1——for c in a:if c in b :b1删除c的计数
Counter({
     'x': 1, 'c': 1})

3. 深复制copy()

自己写的版本:

思路:

  1. 根据chars建立哈希表map1
  2. 根据words中的不同子字符串建立哈希表map2
  3. 两个哈希表进行比较判断
class Solution:
    def countCharacters(self,words, chars) -> int:
        map1 = {
     }
        for i in range(len(chars)):
            if chars[i] in map1.keys():
                map1[chars[i]] += 1
            else:
                map1[chars[i]] = 1
        count=0
        for i in words:
            map2={
     }
            for j in i:
                if j in map2.keys():
                    map2[j]+=1
                else:
                    map2[j]=1
            for k in map2.keys():
                if k in map1.keys():map2[k]-=map1[k]
            if map2 and max(map.values())<=0:
                count+=len(i)
        return count

可以使用深复制第一个哈希表,然后运用for直接在深复制后的哈希表上进行操作:

class Solution:
    def countCharacters(self, words: List[str], chars: str) -> int:
        mine = {
     }
        res = 0
        for c in chars:
            if c not in mine:
                mine[c] = 0
            mine[c] += 1
        for w in words:
            tmp = mine.copy()
            yes = 1
            for t in w:
                if t not in tmp or tmp[t] <= 0:
                    yes=0
                    break
                tmp[t] -= 1
            if yes:
                res+=len(w)
        return res

你可能感兴趣的:(leetcode,数据结构,python,哈希表)