Python蓝桥杯训练:基本数据结构 [哈希表]

news2024/11/26 3:53:10

Python蓝桥杯训练:基本数据结构 [哈希表]

文章目录

  • Python蓝桥杯训练:基本数据结构 [哈希表]
  • 一、哈希表理论基础知识
    • 1、开放寻址法
    • 2、链式法
  • 二、有关哈希表的一些常见操作
  • 三、力扣上面一些有关哈希表的题目练习
    • 1、[有效的字母异位词](https://leetcode.cn/problems/valid-anagram/)
    • 2、[两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/)
    • 3、[快乐数](https://leetcode.cn/problems/happy-number/)
    • 4、[两数之和](https://leetcode.cn/problems/two-sum/)
    • 5、[四数相加Ⅱ](https://leetcode.cn/problems/4sum-ii/)
    • 6、[三数之和](https://leetcode.cn/problems/3sum/)
    • 7、[四数之和](https://leetcode.cn/problems/4sum/)
  • 四、如何选择哈希表实现对象?

本次博客我是通过Notion软件写的,转md文件可能不太美观,大家可以去我的博客中查看:北天的 BLOG,持续更新中,另外这是我创建的编程学习小组频道,想一起学习的朋友可以一起!!!

一、哈希表理论基础知识

哈希表(Hash table)是一种常用的数据结构,它通过使用哈希函数将数据元素映射到数组中的某个位置,并使用链表维护元素间的映射关系。哈希表具有查询、插入、删除等操作的高效性,因此被广泛应用于数据存储、查找和删除等场景。

哈希函数(Hash function)是哈希表的核心部分,它通过一种特定的映射方式,将数据元素映射到数组的某个位置上。一般来说,哈希函数需要满足以下三个条件:

  1. 哈希函数对于任意的数据元素,都能生成一个数组下标;
  2. 对于不同的数据元素,生成的下标尽可能不同;
  3. 对于相同的数据元素,生成的下标始终相同。

如果哈希函数生成的下标相同,我们称之为哈希冲突(Hash collision)。哈希冲突的处理方式有很多种,常见的有开放寻址法和链式法。

1、开放寻址法

当冲突发生时,在下一个空余的位置存储该数据元素。

开放寻址法的思想是当哈希冲突发生时,继续寻找数组中下一个空闲的位置,并将数据元素插入该位置。具体来说,开放寻址法中有三种常用的冲突解决方法:

  • 线性探测(Linear Probing):当哈希冲突发生时,以步长为1,在数组中寻找下一个空闲的位置,并将数据元素插入该位置。
  • 二次探测(Quadratic Probing):当哈希冲突发生时,以步长的平方为增量,在数组中寻找下一个空闲的位置,并将数据元素插入该位置。
  • 双重哈希(Double Hashing):当哈希冲突发生时,使用第二个哈希函数计算出步长,并在数组中寻找下一个空闲的位置,并将数据元素插入该位置。

https://static001.geekbang.org/resource/image/5c/d5/5c31a3127cbc00f0c63409bbe1fbd0d5.jpg

优点:

  • 对于小规模数据集,开放寻址法具有较小的空间开销;
  • 开放寻址法的数据存储在数组中,因此具有较好的局部性,缓存命中率高。

缺点:

  • 开放寻址法对于冲突的解决方式较为固定,不具有链式法灵活的处理方式;
  • 随着数据集的扩大,开放寻址法容易出现聚集现象,即数据元素的分布不均匀,从而导致探测冲突的频率增加,插入和查找效率降低。

应用场景:

  • 适用于小规模数据集,并且需要快速随机访问的场景。

2、链式法

链式法的思想是当哈希冲突发生时,在该位置存储一个链表,并将该数据元素插入链表中。链式法的实现相对简单,具体来说,可以使用数组和链表组合实现,即数组中每个位置存储一个指向链表头节点的指针。

https://static001.geekbang.org/resource/image/a4/7f/a4b77d593e4cb76acb2b0689294ec17f.jpg

优点:

  • 链式法的数据存储在链表中,因此对于大规模数据集,可以有效地解决聚集现象,具有较好的扩展性;
  • 链式法的处理方式较为灵活,能够处理各种类型的哈希冲突。

缺点:

  • 链式法的链表节点需要额外的空间存储指针信息,因此对于空间的使用有一定的额外开销;
  • 链式法的查询效率取决于链表的长度,因此如果链表过长,查询效率会降低。

应用场景:

  • 适用于大规模数据集,并且需要支持高效插入和删除操作的场景。

开放寻址法和链式法都是哈希表解决哈希冲突的有效方法。在选择哪种方法时,需要考虑数据集大小、查询、插入和删除操作的需求以及空间的限制等因素。在实际应用中,常常需要根据实际情况选择适合的哈希表实现方式。

哈希表的查询、插入、删除等操作都是基于哈希函数的。查询操作是通过计算出该数据元素的哈希值,并在数组中找到对应的位置,如果该位置存在该数据元素,则返回该数据元素。插入操作是在查询操作的基础上,在数组中的对应位置插入该数据元素。删除操作是通过查询操作找到该数据元素,并在数组中删除该数据元素。

总的来说,哈希表是一种高效的数据存储和查询结构,它通过使用哈希函数和链表维护数据元素之间的映射关系,在提高数据存储和查询效率的同时,也具有解决哈希冲突的能力。

二、有关哈希表的一些常见操作

Python 中的哈希表实现是通过内置的 **dict**数据类型实现的。下面是一些常见的哈希表操作及其对应的 Python 代码:

  • 插入元素

    使用 **dict[key] = value**进行插入,如果 key 已经存在,会将对应的 value 进行更新。

    d = {}
    d["key1"] = "value1"
    d["key2"] = "value2"
    d["key1"] = "new_value1"  # 更新 key1 对应的值
    
  • 删除元素

    使用 **del dict[key]**进行删除操作,如果 key 不存在,会抛出 KeyError 异常。

    d = {"key1": "value1", "key2": "value2"}
    del d["key1"]
    
  • 查询元素

    使用 **dict[key]**进行查询操作,如果 key 不存在,会抛出 KeyError 异常。

    d = {"key1": "value1", "key2": "value2"}
    value = d["key1"]
    
  • 判断元素是否存在

    可以使用 **key in dict**来判断 key 是否存在于 dict 中。

    d = {"key1": "value1", "key2": "value2"}
    if "key1" in d:
        print("key1 exists in the dictionary")
    
  • 遍历元素

    可以使用 **for key in dict**来遍历 dict 中的所有 key,也可以使用 dict.items() 方法来遍历 dict 中的所有键值对。

    d = {"key1": "value1", "key2": "value2"}
    for key in d:
        print(key, d[key])
    
    for key, value in d.items():
        print(key, value)
    

三、力扣上面一些有关哈希表的题目练习

1、有效的字母异位词

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

示例1:

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

示例2:

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

提示:

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

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


1、字典计数:将两个字符串都转化为字典,然后比较这两个字典是否相等。字典中 key 为字符串中的字符,value 为字符出现的次数。

2、排序:对两个字符串分别进行排序,然后比较两个有序字符串是否相等。

3、哈希表:使用长度为 26 的数组记录每个字符出现的次数,然后比较两个数组是否相等。


class Solution: 
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        dict_s = {}
        dict_t = {}
        for char in s:
            dict_s[char] = dict_s.get(char, 0) + 1
        for char in t:
            dict_t[char] = dict_t.get(char, 0) + 1
        return dict_s == dict_t

该算法的时间复杂度为 O(n),空间复杂度为 O(n)。

方法二:

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

该算法的时间复杂度为 O(nlogn),空间复杂度为 O(n)(由于排序需要使用额外的空间)。

方法三:

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        record = [0] * 26
        for i in s:
            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:
                return False
        return True

该算法的时间复杂度为 O(n),空间复杂度为 O(1)。由于字符串中只包含小写字母,因此数组的长度为 26。

B站上面卡尔老师对于这道题的解题思路是使用哈希表解决,讲得很不错,在这里推荐大家去看看他的教学视频:

https://www.bilibili.com/video/BV1YG411p7BA/?spm_id_from=333.788&vd_source=eae0406d86828f39f6ac3a3b0e8a2a34


2、两个数组的交集

示例1:

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

示例2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

提示:

1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000


class Solution:
    def intersection(self, nums1, nums2):
        # 创建一个空的哈希表
        val_dict = {}
        # 创建一个空的结果列表
        ans = []
        # 遍历数组 nums1 中的元素,将其作为哈希表中的键,对应的值设置为 1
        for num in nums1:
            val_dict[num] = 1
            
        # 遍历数组 nums2 中的元素
        for num in nums2:
            # 如果当前元素在哈希表中存在且对应的值为 1
            if num in val_dict.keys() and val_dict[num] == 1:
                # 将当前元素加入结果列表 ans 中
                ans.append(num)
                # 将哈希表中对应的值设置为 0,以便去重
                val_dict[num] = 0
                
        # 返回结果列表 ans
        return ans

这段代码的时间复杂度同样为 O(m+n),其中 m 和 n 分别为两个数组的长度。

需要注意的是,这段代码中使用了字典的 **keys()**方法来判断某个键是否存在于字典中,但实际上可以直接使用 if num in val_dict:来判断某个键是否存在于字典中,因为在 Python 中,in
操作符默认会在字典的键中查找。另外,在这段代码中,将哈希表中对应的值设置为 0 是为了去重,但实际上不需要这么做,可以直接使用 set 数据类型来实现去重,这样代码会更简洁。

下面是体现Python之美的简洁代码:

class Solution:
    def intersection(self, nums1, nums2):
        nums1 = set(nums1)
        nums2 = set(nums2)
        return list(nums1 & nums2)

更简洁的写法是:

class Solution:
    def intersection(self, nums1, nums2):
        return list(set(nums1) & set(nums2))

如果我们使用暴力枚举法的话,我们可以枚举数组 **nums1**和 **nums2**中的每个元素,判断它们是否相等,如果相等就加入结果集中。具体实现代码如下:

class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        ans = []
        for num1 in nums1:
            for num2 in nums2:
                if num1 == num2 and num1 not in ans:
                    ans.append(num1)
        return ans

这段代码的时间复杂度为 O(mn),其中 m 和 n 分别为两个数组的长度。由于需要对结果集去重,因此还需要额外的时间复杂度,所以使用暴力解法不如使用哈希表或 set 数据类型来实现高效。

B站上面卡尔老师对于这道题的解题思路是使用第一种详细的哈希表法解决,讲得很不错,在这里推荐大家去看看他的教学视频:

https://www.bilibili.com/video/BV1ba411S7wu/?spm_id_from=333.788&vd_source=eae0406d86828f39f6ac3a3b0e8a2a34


3、快乐数

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

示例1:

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

示例2:

输入:n = 2
输出:false

提示:

1 <= n <= 231 - 1

哈希表法的基本思路是,对于一个数字 n,我们按照题目要求进行一系列计算得到一个新的数字 m,然后将 m 保存在哈希表中。如果计算后得到的数字 m 已经在哈希表中出现过了,那么说明我们已经进入了一个循环,这个数不是快乐数。否则,我们继续按照题目要求进行计算,直到得到 1 或者进入循环。

这道题同样可以使用暴力枚举法解决,我们需要不断地按照题目要求计算,直到得到 1 或者进入循环。具体来说,我们可以编写一个计算平方和的函数 squareSum,然后在一个 while 循环中不断调用这个函数,直到得到 1 或者进入循环。在每一轮计算中,我们将当前数字赋值给 n,然后调用 squareSum 计算出一个新的数字 m。如果 m 等于 1,说明这个数是快乐数;否则,我们继续按照题目要求计算,直到得到 1 或者进入循环。


class Solution:
    def isHappy(self, n):
		    seen = set()  # 用来保存已经出现过的数字
		    while n != 1 and n not in seen:
		        seen.add(n)
		        n = sum(int(i) ** 2 for i in str(n))
		    return n == 1

在上面代码中,我们首先定义了一个空的集合 seen,用来保存已经出现过的数字。然后,我们使用一个 while 循环来按照题目要求进行计算,直到得到 1 或者进入循环。在每一轮计算中,我们首先将当前数字 n 加入到 seen 中,然后按照题目要求计算出一个新的数字 m,并将 m 赋值给 n。如果 n 已经出现在 seen 中了,那么说明我们进入了一个循环,可以直接退出循环。最后,我们判断 n 是否等于 1,如果是,返回 True,否则返回 False。

我们还可以换一种实现思路,但还是使用的哈希表法,那就是使用哈希表来记录出现过的数字,每次计算平方和后判断是否出现过,如果出现过则说明出现了循环,否则继续计算。具体来说,代码定义了一个函数 calculate_happy,用来计算一个数每个位置上的数字的平方和。然后,使用一个 **set**类型的哈希表 **record**来记录已经出现过的数字,初始时 **record**是空的。接下来,我们进入一个 while 循环,不断计算 **n**的平方和并更新 **n**的值,直到 **n**等于 1 或者 **n**在 **record**中出现过。在每一轮循环中,我们首先调用 **calculate_happy**计算出 **n**的平和,然后判断平方和是否等于 1,如果是,说明这个数是快乐数,可以直接返回 True。如果平方和在 **record**中出现过,说明我们已经进入了一个循环,可以直接返回 False。否则,将平方和加入到 **record**中,并将 **n**赋值为平方和,继续循环。

具体实现代码如下:

class Solution:
    def isHappy(self, n):
        # 定义一个函数,用来计算一个数每个位置上的数字的平方和
        def calculate_happy(num):
            sum_ = 0
            while num:
                sum_ += (num % 10) ** 2
                num = num // 10
                
            return sum_
        
        # 定义一个 set 类型的哈希表 record,用来记录已经出现过的数字
        record = set()
        
        # 进入一个 while 循环,不断计算 n 的平方和并更新 n 的值,直到 n 等于 1 或者 n 在 record 中出现过
        while True:
            # 计算 n 的平方和
            n = calculate_happy(n)
            # 如果 n 等于 1,说明这个数是快乐数,可以直接返回 True
            if n == 1:
                return True
            # 如果平方和在 record 中出现过,说明我们已经进入了一个循环,可以直接返回 False
            if n in record:
                return False
            # 否则,将平方和加入到 record 中,并将 n 赋值为平方和,继续循环
            else:
                record.add(n)

如果我们使用暴力枚举法解决的话,具体实现思路是:我们需要不断地按照题目要求计算,直到得到 1 或者进入循环。具体来说,我们可以编写一个计算平方和的函数 squareSum,然后在一个 while 循环中不断调用这个函数,直到得到 1 或者进入循环。在每一轮计算中,我们将当前数字赋值给 n,然后调用 squareSum 计算出一个新的数字 m。如果 m 等于 1,说明这个数是快乐数;否则,我们继续按照题目要求计算,直到得到 1 或者进入循环。

def squareSum(n: int) -> int:
    """计算一个数每个位置上的数字的平方和"""
    res = 0
    while n > 0:
        digit = n % 10
        res += digit * digit
        n //= 10
    return res

def isHappy(n: int) -> bool:
    seen = set()  # 用来保存已经出现过的数字
    while n != 1 and n not in seen:
        seen.add(n)
        n = squareSum(n)
    return n == 1

在上面代码中,我们首先定义了一个计算平方和的函数 squareSum,这个函数会计算一个数每个位置上的数字的平方和。然后,我们使用一个 while 循环来按照题目要求进行计算,直到得到 1 或者进入循环。在每一轮计算中,我们首先将当前数字 n 加入到 seen 中,然后调用 squareSum 计算出一个新的数字 m,并将 m 赋值给 n。如果 n 已经出现在 seen 中了,那么说明我们进入了一个循环,可以直接退出循环。最后,我们判断 n 是否等于 1,如果是,返回 True,否则返回 False。

使用暴力枚举法虽然简单,但是时间复杂度较高,无法通过力扣的所有测试用例。因此,我们更推荐使用哈希表法来解决这个问题。


4、两数之和

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

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

示例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]

提示:

2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案

**进阶:**你可以想出一个时间复杂度小于 O(n2)的算法吗?


  1. 定义一个哈希表 hash_map,用来记录每个数的下标。
  2. 遍历数组 nums,对于每个数 num,计算出需要的另一个数 target - num,并在哈希表中查找是否存在这个数。
  3. 如果存在,则直接返回这两个数的下标。
  4. 如果不存在,则将当前数的下标和值加入哈希表中,继续遍历数组。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        # 定义一个哈希表,用来记录每个数的下标
        hash_map = {}
        
        # 遍历数组 nums,对于每个数 num,计算出需要的另一个数 target - num,并在哈希表中查找是否存在这个数
        for i, num in enumerate(nums):
            complement = target - num
            if complement in hash_map:
                # 如果存在,则直接返回这两个数的下标
                return [hash_map[complement], i]
            else:
                # 如果不存在,则将当前数的下标和值加入哈希表中,继续遍历数组
                hash_map[num] = i
        
        # 如果遍历完数组都没有找到满足条件的数,说明输入数据有误,返回空列表
        return []

在解决这个问题时,我们首先要考虑如何遍历数组,并对于每个数计算出需要的另一个数。然后,我们可以使用哈希表来记录每个数的下标,这样在查找另一个数时可以快速地定位。最后,我们需要考虑一些特殊情况,比如输入数据为空或者没有满足条件的数。

如果我们使用暴力枚举法解决的话,我们需要对于每个数,依次遍历剩下的数,查找是否存在满足条件的数。

具体思路如下:

  1. 遍历数组 nums,对于每个数 num1,依次遍历剩下的数 num2
  2. 如果找到两个数的和等于目标值 target,则返回它们的下标。
  3. 如果遍历完整个数组都没有找到满足条件的数,说明输入数据有误,返回空列表。

具体实现代码是:

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        # 遍历数组 nums,对于每个数 num1,依次遍历剩下的数 num2
        for i, num1 in enumerate(nums):
            for j, num2 in enumerate(nums[i+1:]):
                j += i + 1  # 由于 j 是相对于 i 的偏移量,所以需要加上 i+1
                
                # 如果找到两个数的和等于目标值 target,返回它们的下标
                if num1 + num2 == target:
                    return [i, j]
        
        # 如果遍历完整个数组都没有找到满足条件的数,说明输入数据有误,返回空列表
        return []

需要注意的是,在这个算法中,我们需要对于每个数依次遍历剩下的数,时间复杂度为 O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度。因此,如果输入数据很大,这个算法可能会超时。而使用哈希表的方法可以将时间复杂度降低到 O ( n ) O(n) O(n)

B站上面卡尔老师对于这道题的解题思路是使用第一种详细的哈希表法解决,讲得很不错,在这里推荐大家去看看他的教学视频:

https://www.bilibili.com/video/BV1aT41177mK/?spm_id_from=333.788&vd_source=eae0406d86828f39f6ac3a3b0e8a2a34


5、四数相加Ⅱ

  • 0 <= i, j, k, l < n
  • 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

示例2:

输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

提示:

n == nums1.length
n == nums2.length
n == nums3.length
n == nums4.length
1 <= n <= 200
-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228

除了上述方法之外我们还可以使用Python 内置的 **defaultdict来解决这个问defaultdic**是一个类似于字典(dictionary)的容器类型,它是 Python 标准库 collections 中的一种数据类型,支持所有字典所支持的操作,但是在键不存在时,返回一个默认值(可以自己设定),而不是抛出 KeyError 异常。

这道题不推荐使用暴力,因为这样时间复杂度很高是通过不了的。


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

时间复杂度为 O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度。虽然使用了哈希表来优化查找过程,但仍然需要对数组进行遍历。如果输入数据很大,这个算法可能会超时。

使用Python内置函数解决:

from collections import defaultdict

class Solution:
    def fourSumCount(self, nums1, nums2, nums3, nums4):
        count_dict = defaultdict(int)
        for i in nums1:
            for j in nums2:
                count_dict[i + j] += 1
        count = 0
        for i in nums3:
            for j in nums4:
                count += count_dict[-(i + j)]
        return count

上述代码具体实现步骤如下:

  1. 使用 defaultdict(int) 来创建一个默认值为 0 的字典 count_dict,用于存储 nums1 和 nums2 中元素的和出现的次数。
  2. 遍历 nums1 和 nums2,将它们的和作为字典 count_dict 的键,值加 1。
  3. 遍历 nums3 和 nums4,计算它们的和的相反数,并在字典 count_dict 中查找对应的值,将值累加到计数器 count 中。
  4. 返回计数器 count 的值。

这种实现方式也是时间复杂度为 O ( n 2 ) O(n^2) O(n2) 的,其中 n n n 是数组的长度。

B站上面卡尔老师对于这道题的解题思路是使用第一种哈希表法解决,讲得很不错,在这里推荐大家去看看他的教学视频:

https://www.bilibili.com/video/BV1Md4y1Q7Yh/?spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=eae0406d86828f39f6ac3a3b0e8a2a34


6、三数之和

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1][-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0

示例3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0

提示:

3 <= nums.length <= 3000
-105 <= nums[i] <= 105

首先我们来思考分析题目问题有哪些解决方法,大概就是下面这几种:

  1. 首先,可以考虑暴力解法。暴力解法的思路是枚举所有的三元组,时间复杂度为 O ( n 3 ) O(n^3) O(n3),不太可取。
  2. 考虑优化暴力解法。可以使用双指针的方法,将时间复杂度降为 O ( n 2 ) O(n^2) O(n2)
  3. 进一步优化。可以先对数组进行排序,然后再使用双指针的方法,时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( l o g n ) O(log n) O(logn)
  4. 可以使用哈希表的方法,时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)

由我们的分析可以看出来最优解法为第三种方法,即先对数组进行排序,再使用双指针的方法。

使用双指针的思路大致为:

  1. 对 nums 数组进行排序,排序后从小到大依次枚举每个数作为 a,同时设定两个指针 l 和 r 分别指向 a 的后面和数组末尾,即 l = a + 1,r = n - 1。
  2. 在 l 和 r 的指针范围内,判断 b + c 的值与 -a 的大小关系,如果 b + c > -a,则将指针 r 向左移动;如果 b + c < -a,则将指针 l 向右移动;如果 b + c = -a,则找到了一组解,将其加入结果列表中。
  3. 对于每个枚举到的数 a,如果它和它前面的数相等,就跳过这个数,避免重复计算。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()  # 对数组进行排序
        n = len(nums)
        ans = []
        for i in range(n):
            if i > 0 and nums[i] == nums[i - 1]:  # 跳过重复的数
                continue
            l, r = i + 1, n - 1  # 双指针初始值
            while l < r:
                if nums[l] + nums[r] + nums[i] == 0:  # 找到一组符合条件的数
                    ans.append([nums[i], nums[l], nums[r]])  # 添加到结果集中
                    while l < r and nums[l] == nums[l + 1]:  # 跳过重复的数
                        l += 1
                    while l < r and nums[r] == nums[r - 1]:  # 跳过重复的数
                        r -= 1
                    l += 1  # 左指针右移
                    r -= 1  # 右指针左移
                elif nums[l] + nums[r] + nums[i] < 0:  # 三数之和小于0
                    l += 1  # 左指针右移
                else:  # 三数之和大于0
                    r -= 1  # 右指针左移
        return ans  # 返回结果

上述代码的详细实现步骤如下:

  1. 首先对数组进行排序,方便后续处理;
  2. 遍历数组中的每个数作为三元组中的第一个数;
  3. 对于每个三元组中的第一个数,使用双指针来找到剩余两个数;
  4. 在双指针中,l 指向当前三元组中的第二个数,r 指向当前三元组中的第三个数;
  5. 当三个数的和等于0时,即找到了一个符合要求的三元组,将其添加到答案数组中,并分别将 l 和 r 向中间移动一位,跳过重复的数;
  6. 当三个数的和小于0时,将 l 向右移动一位,尝试增大当前三元组的和;
  7. 当三个数的和大于0时,将 r 向左移动一位,尝试减小当前三元组的和;
  8. 最后返回答案数组。

除了双指针算法之外,还有一些其他的解决方法,如哈希表、暴力枚举等。但是,在时间复杂度和空间复杂度方面,双指针算法是效率最高的解决方法。

下面我给出暴力枚举法和哈希表法供大家学习参考。

暴力枚举法:

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        ans = []
        # 枚举三个数的组合
        for i in range(n):
            for j in range(i+1, n):
                for k in range(j+1, n):
                    if nums[i] + nums[j] + nums[k] == 0:
                        ans.append([nums[i], nums[j], nums[k]])
        ans = [list(t) for t in set(tuple(lst) for lst in ans)] # 去重
        return ans

这种方法的时间复杂度较高,不适合处理较大的数据集,但对于小数据集可以得到正确的结果。

哈希表法:

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        ans = []
        hash_table = {}
        for i in range(n):
            hash_table[nums[i]] = i
        
        for i in range(n):
            for j in range(i+1, n):
                if -nums[i]-nums[j] in hash_table and hash_table[-nums[i]-nums[j]] > j:
                    ans.append([nums[i], nums[j], -nums[i]-nums[j]])
        
        ans = [list(t) for t in set(tuple(lst) for lst in ans)] # 去重
        return ans

更推荐大家去使用双指针法来解决这个问题。

B站上面卡尔老师对于这道题的解题思路是使用第一种的双指针法解决,讲得很不错,在这里推荐大家去看看他的教学视频:

https://www.bilibili.com/video/BV1GW4y127qo/?spm_id_from=333.788&vd_source=eae0406d86828f39f6ac3a3b0e8a2a34


7、四数之和

  • 0 <= a, b, c, d < n
  • a、b、c 和 d 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

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

示例1:

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

示例2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109

暴力枚举法我们可以使用四重循环来枚举四个元素,这种方法的时间复杂度为 O ( n 4 ) O(n^4) O(n4),不太可取。

这里我们只讲一下其中的双指针法和哈希表。

其中双指针法具体思路和代码实现可以参考第 15 题 三数之和 的解题思路,先对数组进行排序,然后在数组中枚举第一个数,在剩下的数中使用双指针的方法寻找满足条件的另外两个数,使得三个数的和等于 target。在寻找的过程中,为了避免重复的解,需要加入一些特判和判断。

然后哈希表法是先将数组 nums 中的两个元素相加,将它们的和作为哈希表的键,它们的下标作为哈希表的值。接下来,遍历数组 nums 中的另外两个元素,如果它们的和等于 target 减去哈希表中的键,就说明找到了符合要求的四元组。


class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        n = len(nums)
        if n < 4:  # 数组长度小于 4 时,直接返回空列表
            return []
        nums.sort()  # 对数组进行排序
        res = []  # 存储结果的列表
        for i in range(n-3):  # 枚举第一个数
            if i > 0 and nums[i] == nums[i-1]:  # 跳过重复的数
                continue
            if nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target:  # 如果当前的最小值都大于 target,则结束
                break
            if nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target:  # 如果当前的最大值都小于 target,则跳过
                continue
            for j in range(i+1, n-2):  # 枚举第二个数
                if j > i+1 and nums[j] == nums[j-1]:  # 跳过重复的数
                    continue
                if nums[i] + nums[j] + nums[j+1] + nums[j+2] > target:  # 如果当前的最小值都大于 target,则结束
                    break
                if nums[i] + nums[j] + nums[n-2] + nums[n-1] < target:  # 如果当前的最大值都小于 target,则跳过
                    continue
                left, right = j+1, n-1  # 双指针初始值
                while left < right:  # 双指针遍历
                    temp_sum = nums[i] + nums[j] + nums[left] + nums[right]  # 计算当前四数之和
                    if temp_sum == target:  # 如果等于 target,则加入结果,并跳过重复的数
                        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
                    elif temp_sum < target:  # 如果小于 target,则左指针右移
                        left += 1
                    else:  # 如果大于 target,则右指针左移
                        right -= 1
        return res  # 返回结果列表

在上述代码中,我们首先对数组进行排序,然后依次枚举前两个数的下标 ij。在每次枚举中,我们通过双指针的方式,从剩下的数中找到另外两个数,使得四数之和等于目标值 target。在这个过程中,需要注意以下几点:

  1. 为了避免重复,我们需要对每一个枚举到的数都跳过所有与之前枚举到的数相同的情况。
  2. 当四数之和小于目标值时,需要将左指针右移一位,以使得和变大;当四数之和大于目标值时,需要将右指针左移一位,以使得和变小。
  3. 对于重复的数,我们只需将指针移动到第一个不同的数处即可。

最终,我们可以得到所有不重复的四元组,使得它们的和等于目标值 target

使用哈希表解决:

class Solution(object):
    def fourSum(self, nums, target):
        # 创建一个哈希表,记录每个数字出现的次数。
        hashmap = dict()
        for n in nums:
            if n in hashmap:
                hashmap[n] += 1
            else: 
                hashmap[n] = 1

        # 创建一个空的集合,用于存储结果。
        ans = set()

        # 使用三重循环枚举三个数(i,j,k),其中i<j<k。
        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 hashmap:
                        # 计算差值在三个数中出现的次数,如果大于等于当前哈希表中该差值的出现次数,则将四个数进行排序,并添加到集合中。
                        count = (nums[i] == val) + (nums[j] == val) + (nums[k] == val)
                        if hashmap[val] > count:
                            ans_tmp = tuple(sorted([nums[i], nums[j], nums[k], val]))
                            ans.add(ans_tmp)
                        else:
                            continue

        # 返回集合的列表形式作为最终结果。
        return list(ans)

B站上面卡尔老师对于这道题的解题思路是使用第一种的双指针法解决,讲得很不错,在这里推荐大家去看看他的教学视频:

https://www.bilibili.com/video/BV1DS4y147US/?spm_id_from=333.788&vd_source=eae0406d86828f39f6ac3a3b0e8a2a34

四、如何选择哈希表实现对象?

在上面的题目中,我们在定义选择哈希表的实现对象的时候是不一样的,有些朋友可能就会好奇我们什么时候选择数组作为哈希表更好,什么时候选择set作为哈希表更好,什么时候选择map作为哈希表更好?

下面我来做一个总结,仅供参考:

  1. 数组作为哈希表
    如果题目中明确说明了数据的范围,可以直接利用数组实现哈希表。例如,当题目要求哈希表中的键(即数组中的元素)取值范围为[0, 100]时,可以定义长度为101的数组作为哈希表,键值为i的元素就存放在下标为i的位置上。这种方法的时间复杂度为O(1),是最快的,但需要额外的空间来存储数组。
  2. set作为哈希表
    如果不需要对哈希表中的键进行计数,而只是需要判断某个元素是否存在于哈希表中,可以使用set作为哈希表。这种方法的时间复杂度为O(1),比使用数组作为哈希表更节省空间。
  3. map作为哈希表
    如果需要对哈希表中的键进行计数,可以使用map作为哈希表。map可以实现键值对的映射,并记录每个键在哈希表中出现的次数。这种方法的时间复杂度为O(1),但空间开销较大。

在实际应用中,我们需要根据题目的要求来选择适合的哈希表实现对象。具体而言,当哈希表中的键取值范围不大,且需要对键进行计数时,可以选择数组或map作为哈希表;当哈希表中的键取值范围较大,且只需要判断键是否存在时,可以选择set作为哈希表。

表格总结如下:

哈希表实现对象适用场景时间复杂度空间复杂度
数组键取值范围小,需要对键进行计数O(1)额外空间
set键取值范围大,只需判断键是否存在O(1)额外空间
map键取值范围小或大,需要对键进行计数,且不需要自定义哈希函数O(1)额外空间

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/373391.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

0101基础概念-图-数据结构和算法(Java)

文章目录1 图1.1 定义1.2 4种图模型2 无向图2.1 定义2.2 术语后记1 图 1.1 定义 图是一种非线性的数据结构&#xff0c;表示多对多的关系。 图&#xff08;Graph&#xff09;是由顶点的有穷非空集合和顶点之间边的集合组成&#xff0c;通常表示为&#xff1a;G(V, E)&#xf…

ecology9-谷歌浏览器下-pdf.js在渲染时部分发票丢失文字 问题定位及解决

问题 问题描述 &#xff1a; 在谷歌浏览器下&#xff0c;pdf.js在渲染时部分发票丢失文字&#xff1b;360浏览器兼容模式不存在此问题 排查思路&#xff1a;1、对比谷歌浏览器的css样式和360浏览器兼容模式下的样式&#xff0c;没有发现关键差别 2、✔使用Fiddler修改网页js D…

什么是线程死锁?如何解决死锁问题

死锁&#xff0c;一组互相竞争的资源的线程之间相互等待&#xff0c;导致永久阻塞的现象。 如下图所示&#xff1a; 与死锁对应的&#xff0c;还有活锁&#xff0c;是指线程没有出现阻塞&#xff0c;但是无限循环。 有一个经典的银行转账例子如下&#xff1a; 我们有个账户类…

操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权

系列文章 操作系统权限提升(十二)之绕过UAC提权-Windows UAC概述 操作系统权限提升(十三)之绕过UAC提权-MSF和CS绕过UAC提权 注&#xff1a;阅读本编文章前&#xff0c;请先阅读系列文章&#xff0c;以免造成看不懂的情况&#xff01;&#xff01; 基于白名单AutoElevate绕过…

2-MATLAB APP Design-下拉菜单栏的使用

一、APP 界面设计展示 1.新建一个空白的APP,在此次的学习中,我们会用到编辑字段(文本框)、下拉菜单栏、坐标区,首先在界面中拖入一个编辑字段(文本框),在文本框中输入内容:下拉菜单栏的使用,调整背景颜色,字体的颜色为黑色,字体的大小调为26. 2.在左侧组件库常用栏…

Qt音视频开发17-vlc内核回调拿图片进行绘制

一、前言 在众多播放器中&#xff0c;支持的种类格式众多&#xff0c;并支持DVD影音光盘&#xff0c;VCD影音光盘及各类流式协议&#xff0c;提供了sdk进行开发&#xff0c;这点是至关重要的&#xff0c;尽管很多优秀的播放器很牛逼&#xff0c;由于没有提供sdk第三方开发&…

【网络编程套接字(一)】

网络编程套接字&#xff08;一&#xff09;理解源IP地址和目的IP地址理解源MAC地址和目的MAC地址理解源端口号和目的端口号PORT VS PID认识TCP协议和UDP协议网络字节序socket编程接口socket常见APIsockaddr结构简单的UDP网络程序服务端创建套接字服务端绑定字符串IP VS 整数IP客…

面试官: 你知道 JWT、JWE、JWS 、JWK嘛?

想起了 之前做过的 很多 登录授权 的项目 它相比原先的session、cookie来说&#xff0c;更快更安全&#xff0c;跨域也不再是问题&#xff0c;更关键的是更加优雅 &#xff0c;所以今天总结了一篇文章来介绍他 JWT 指JSON Web Token&#xff0c;如果在项目中通过 jjwt 来支持 J…

Springboot整合 Thymeleaf增删改查一篇就够了

很早之前写过Thymeleaf的文章&#xff0c;所以重新温习一下&#xff0c;非前后端分离&#xff0c;仅仅只是学习 官网&#xff1a; https://www.thymeleaf.org/ SpringBoot可以快速生成Spring应用&#xff0c;简化配置&#xff0c;自动装配&#xff0c;开箱即用。 JavaConfigur…

【java基础】枚举类(enum)

文章目录基本介绍快速使用字段、方法、构造器枚举类方法toString方法valueOf方法values方法ordinal方法基本介绍 在java中有一种特殊的类型就是枚举类&#xff0c;对于一个有限的有固定值的集合&#xff0c;我们就可以考虑使用枚举类来进行表示&#xff0c;例如服装的大小为 小…

linux shell 入门学习笔记15 shell 条件测试

概念 shell的条件测试目的是得出真和假。 shell 提供的条件测试语法 test 命令 [] 中括号命令 语法*&#xff1a; test条件测试 test命令用来评估一个表达式&#xff0c;他的结果是真&#xff0c;还是假&#xff0c;如果条件为真&#xff0c;那么命令执行状态结果就为0&…

【蓝桥杯集训·周赛】AcWing 第92场周赛

文章目录第一题 AcWing 4864. 多边形一、题目1、原题链接2、题目描述二、解题报告1、思路分析2、时间复杂度3、代码详解第二题 AcWing 4865. 有效类型一、题目1、原题链接2、题目描述二、解题报告1、思路分析2、时间复杂度3、代码详解第三题 AcWing 4866. 最大数量一、题目1、原…

Spring之丐版IOC实现

文章目录IOC控制反转依赖注入Bean的自动装配方式丐版IOC实现BeanDefinition.javaResourceLoader.javaBeanRegister.javaBean和DI的注解BeanFactory.javaApplicationContext测试&#xff0c;实现在这里插入图片描述大家好&#xff0c;我是Leo。Spring核心中依赖注入和IOC容器是非…

JWT令牌

1.普通令牌的问题 客户端申请到令牌&#xff0c;接下来客户端携带令牌去访问资源&#xff0c;到资源服务器将会校验令牌的合法性。 从第4步开始说明&#xff1a; 1、客户端携带令牌访问资源服务获取资源。 2、资源服务远程请求认证服务校验令牌的合法性 3、如果令牌合…

QML Image and Text(图像和文字)

Image&#xff08;图片&#xff09; 图像类型显示图像。 格式&#xff1a; Image {source: "资源地址" } source&#xff1a;指定资源的地址 自动检测文件拓展名&#xff1a;source中的URL 指示不存在的本地文件或资源&#xff0c;则 Image 元素会尝试自动检测文件…

MVI 架构更佳实践:支持 LiveData 属性监听

前言MVI架构为了解决MVVM在逻辑复杂时需要写多个LiveData(可变不可变)的问题,使用ViewState对State集中管理&#xff0c;只需要订阅一个 ViewState 便可获取页面的所有状态通过集中管理ViewState&#xff0c;只需对外暴露一个LiveData&#xff0c;解决了MVVM模式下LiveData膨胀…

Linux_vim编辑器入门级详细教程

前言&#xff08;1&#xff09;vim编辑器其实本质上就是对文本进行编辑&#xff0c;比如在.c文件中改写程序&#xff0c;在.txt文件写笔记什么的。一般来说&#xff0c;我们可以在windows上对文本进行编译&#xff0c;然后上传给Linux。但是有时候我们可能只是对文本进行简单的…

MySQL运维

目录 1、日志 1、错误日志 2、二进制日志 3、查询日志 4、慢查询日志 2、主从复制 搭建 1、主库配置 2、从库配置 3、分库分表 1、简介 ​编辑 1、垂直拆分 2、水平拆分 3、实现技术 2、MyCat 3、MyCat使用和配置 配置 4、MyCat分片 1、垂直拆分 2、水平拆分…

线材-电子线载流能力

今天来讲的是关于电子线的一个小知识&#xff0c;可能只做板子的工程师遇到此方面的问题会比较少&#xff0c;做整机的工程师则必然会遇到此方面问题&#xff0c;那就是线材问题。 下面主要说下电子线的过电流能力。&#xff08;文末有工具下载&#xff09;电子线&#xff08;h…

vue3+rust个人博客建站日记2-确定需求

反思 有人说过我们正在临近代码的终结点。很快&#xff0c;代码就会自动产生出来&#xff0c;不需要再人工编写。程序员完全没用了&#xff0c;因为商务人士可以从规约直接生成程序。 扯淡&#xff01;我们永远抛不掉代码&#xff0c;因为代码呈现了需求的细节。在某些层面上&a…