代码随想录算法训练营第七天|第454题.四数相加II 383. 赎金信 第15题. 三数之和 第18题. 四数之和

第454题.四数相加II

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。

例如:

输入:

  • A = [ 1, 2]
  • B = [-2,-1]
  • C = [-1, 2]
  • D = [ 0, 2]

输出:

2

解释:

两个元组如下:

  1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
  2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

思路:老实说,一开始基本没有什么思路,直接看的文字解析。看到记录数组1和2的加和后豁然开朗,有时候刷题的时候很想避免复杂度为n²的方法,有时候反而影响了自己的思路。记录了1和2的加和后,接下来只需要遍历3和4的加和,当加和为所记录的相反数时即符合题目条件,在这里需要注意之前的加和可能重复出现,所以要用dict来记录出现过的【次数】,而不是【是否出现过】,然后加和即完成,返回count。

代码如下:

class Solution:

    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:

        hashmap = dict()

        for i in nums1:

            for j in nums2:

                hashmap[i+j] = hashmap.get(i+j, 0) + 1      # 记录数组1和2的加和,且记录相应个数

        count = 0

        for i in nums3:

            for j in nums4:

                if -i-j in hashmap:                 # 要加和为0,也就是数组3和4加和等于前面记录的相反数

                    count += hashmap[-i-j]                  

        return count

其他方法,使用defaultdict:

from collections import defaultdict

class Solution:

    def fourSumCount(self, nums1: list, nums2: list, nums3: list, nums4: list) -> int:

        rec, cnt = defaultdict(lambda : 0), 0

        for i in nums1:

            for j in nums2:

                rec[i+j] += 1

        for i in nums3:

            for j in nums4:

                cnt += rec.get(-(i+j), 0)

        return cnt

由于默认value值即为0,所以写法上更具有可读性。

383. 赎金信

给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。

(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)

注意:

你可以假设两个字符串均只含有小写字母。

canConstruct("a", "b") -> false
canConstruct("aa", "ab") -> false
canConstruct("aa", "aab") -> true

给人找回自信的一题,难度较小。思路一开始就很清晰,ransom的字符只有从magazine中获得才满足条件,所以应该最开始将magazine中的字符记录在字典中同时记录出现过的次数。然后开始遍历ransom的字符,当ransom 的所有字符都在字典中记录过并且在ransom中出现的次数小于等于字典中的次数时,满足条件返回True。若字典中不存在该字符或者出现次数小于ransom中的次数,返回False。

代码如下:

class Solution:

    def canConstruct(self, ransomNote: str, magazine: str) -> bool:

        hashmap = dict()

        for i in magazine:

            hashmap[i] = hashmap.get(i, 0) + 1

        for j in ransomNote:

            if hashmap.get(j, 0) == 0:

                return False

            else:

                hashmap[j] -= 1

        return True

但看完解析后发现,其实好像还是用数组好点,给出代码随想录的解释:

一些同学可能想,用数组干啥,都用map完事了,其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!

不过难度一样很简单实现,代码不贴出来了,补充一些其他方法:

Counter代码:

from collections import Counter



class Solution:

    def canConstruct(self, ransomNote: str, magazine: str) -> bool:

        return not Counter(ransomNote) - Counter(magazine)

使用count(简单易懂):

class Solution:

    def canConstruct(self, ransomNote: str, magazine: str) -> bool:

        for char in ransomNote:

            if char in magazine and ransomNote.count(char) <= magazine.count(char):

                continue

            else:

                return False

        return True

两行代码解决的事情,所以还是要积累经验,多用用python里的标准类。。

第15题. 三数之和

思路:一开始的思路肯定是a+b记录在哈希表,然后遍历c看看是否存在-(a+b),但需要去重,妥妥的n²中非常低效的写法。(而且前面的题目解析中已经被剧透说这两题不适合哈希法,果断放弃看解析了。。),看了解析后再一次认识到自己的基础不牢,复习复健的路还很长。。

解析给的是双指针法,对自己很有启发。首先将数组进行排序,然后定义三个指针,分别是进行遍历的i指针,其次是未遍历的部分的头和尾left和right指针。

计算过程是,将三个指针指向的数值进行加和,判断是否等于0,如果等于可以直接记录;否则如果大于0,说明加和数字大了,由于已经排过序,需要让加和变小的话就需要让right指针向左移,同理,如果小于0则需要让left指针向右移,直到left和right相遇。

另外还需要考虑去重的情况,由于i在进行遍历的时候,下一次i遍历到的可能与上一次相等,这样的话会导致三元组的第一个元素和上一次的情况重复,得出的结果会是一样的,需要注意,这里的判断条件应该是i和i-1进行比较,而不是i+1和i比较,否则i和后面的元素比较去重的话,得出的结果是不会出现相同元素的,因为在排序后的数组中,这样i一定是从重复元素之中的最后一个开始的。

贴一段解析,这一部分个人认为理解和代码一样重要,需要重点掌握这里的思考过程:

去重逻辑的思考

#a的去重

说到去重,其实主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i],nums[left],nums[right]

a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。

但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。

有同学可能想,这不都一样吗。

其实不一样!

都是和 nums[i]进行比较,是比较它的前一个,还是比较它的后一个。

如果我们的写法是 这样:

if (nums[i] == nums[i + 1]) { // 去重操作

    continue;

}

那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。

我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!

所以这里是有两个重复的维度。

那么应该这么写:

if (i > 0 && nums[i] == nums[i - 1]) {

    continue;

}

这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。

这是一个非常细节的思考过程。

#b与c的去重

很多同学写本题的时候,去重的逻辑多加了 对right 和left 的去重:(代码中注释部分)

while (right > left) {

    if (nums[i] + nums[left] + nums[right] > 0) {

        right--;

        // 去重 right

        while (left < right && nums[right] == nums[right + 1]) right--;

    } else if (nums[i] + nums[left] + nums[right] < 0) {

        left++;

        // 去重 left

        while (left < right && nums[left] == nums[left - 1]) left++;

    } else {

    }

}

但细想一下,这种去重其实对提升程序运行效率是没有帮助的。

拿right去重为例,即使不加这个去重逻辑,依然根据 while (right > left) 和 if (nums[i] + nums[left] + nums[right] > 0) 去完成right-- 的操作。

多加了 while (left < right && nums[right] == nums[right + 1]) right--; 这一行代码,其实就是把 需要执行的逻辑提前执行了,但并没有减少 判断的逻辑。

最直白的思考过程,就是right还是一个数一个数的减下去的,所以在哪里减的都是一样的。

所以这种去重 是可以不加的。 仅仅是把去重的逻辑提前了而已。

理解后自己写了代码(更多有点像默写,有两行代码没考虑到卡了一会儿):

class Solution:

    def threeSum(self, nums: List[int]) -> List[List[int]]:

        result = []

        length = len(nums)

        nums.sort()

        for i in range(length-2):

            if nums[i] > 0:

                return result

           

            if i>0 and nums[i] == nums[i-1]:

                continue



            left = i+1

            right = length-1

            while left < right:

                if nums[i] + nums[left] + nums[right] == 0:

                    result.append([nums[i], nums[left], nums[right]])

                    while left+1<right and nums[left+1]==nums[left]:

                        left += 1

                    while right-1>left and nums[right-1]==nums[right]:

                        right -= 1

                    left += 1

                    right -= 1              # 这里两个没写,将会一直卡在满足条件加到数组中这个操作循环中,第一次在这里卡了bug



                elif nums[i] + nums[left] + nums[right] < 0:

                    left += 1

                elif nums[i] + nums[left] + nums[right] > 0:

                    right -= 1

        return result

其他方法,使用字典:

class Solution:

    def threeSum(self, nums: List[int]) -> List[List[int]]:

        result = []

        nums.sort()

        # 找出a + b + c = 0

        # a = nums[i], b = nums[j], c = -(a + b)

        for i in range(len(nums)):

            # 排序之后如果第一个元素已经大于零,那么不可能凑成三元组

            if nums[i] > 0:

                break

            if i > 0 and nums[i] == nums[i - 1]: #三元组元素a去重

                continue

            d = {}

            for j in range(i + 1, len(nums)):

                if j > i + 2 and nums[j] == nums[j-1] == nums[j-2]: # 三元组元素b去重

                    continue

                c = 0 - (nums[i] + nums[j])

                if c in d:

                    result.append([nums[i], nums[j], c])

                    d.pop(c) # 三元组元素c去重

                else:

                    d[nums[j]] = j

        return result

第18题. 四数之和

思路:一下想到了三数之和,脑子懒得动了直接套多一层循环,同样的方法,先排序,然后循环i和j再设置left和right指针,当四数加和大了则移动right指针,小了则移动left指针,如果相等则加入结果列表,同时进行去重移动left和right指针。对i和j去重的时候也需要考虑是和前一个数进行对比。

代码实现如下:

class Solution:

    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:

        result = []

        nums.sort()

        length = len(nums)

        for i in range(length-3):



            if i>0 and nums[i]==nums[i-1]:

                continue

            j = i+1

            while j < length-2:

                if j>i+1 and nums[j]==nums[j-1]:

                    j += 1

                    continue

                left = j+1

                right = length-1

                while left < right:

                    if nums[i]+nums[j]+nums[left]+nums[right] < target:

                        left += 1

                    elif nums[i]+nums[j]+nums[left]+nums[right] > target:

                        right -= 1

                    else:

                        result.append([nums[i], nums[j], nums[left], nums[right]])

                        left += 1

                        right -= 1

                        while left<right and nums[left]==nums[left-1]:

                            left += 1

                        while  left<right and nums[right]==nums[right+1]:

                            right -= 1

                j += 1

        return result

其他方法:使用字典

class Solution(object):

    def fourSum(self, nums, target):

        """

        :type nums: List[int]

        :type target: int

        :rtype: List[List[int]]

        """

        # 创建一个字典来存储输入列表中每个数字的频率

        freq = {}

        for num in nums:

            freq[num] = freq.get(num, 0) + 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] > count:

                            ans.add(tuple(sorted([nums[i], nums[j], nums[k], val])))

       

        return [list(x) for x in ans]

关于这个方法,一开始不太理解怎么对数组去重的,所以查了一下,其实是通过元组来进行去重的。

引用AI:

代码中的去重是通过将四元组转换为元组的方式实现的。在Python中,元组是不可变且可哈希的,这意味着它们可以作为字典的键或集合的元素。由于集合不允许重复的元素,所以当一个元组被添加到集合中时,如果已经存在一个相同的元组,它不会被再次添加。

在这段代码中,每次找到一个可能的四元组时,都会通过 tuple(sorted([...])) 将其转换为一个排序后的元组。这样做有两个目的:

  1. 去重:由于集合不允许重复的元素,排序后的元组确保了即使有多个相同的四元组,也只会被添加一次到集合中。
  2. 保持顺序:通过排序,可以确保四元组中的元素顺序一致,这样即使在不同的迭代中找到了相同的四个数字,只要它们的顺序相同,就会被认为是同一个四元组。

例如,如果数组中有 [1, 0, -1, 0, -2, 2],目标和为 0,那么 [-2, -1, 1, 2] 和 [-1, -2, 1, 2] 都满足条件,但由于它们被转换为排序后的元组 (-2, -1, 1, 2),它们在集合中被视为相同的元素,因此不会重复添加。

因此,代码通过使用集合和元组来确保结果中没有重复的四元组。

第二十二天的算法训练营主要涵盖了Leetcode目中的三道目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209目要求在给定的数组中找到长度最小的子数组,使得子数组的和大于等于给定的目标值。这里可以使用滑动窗口的方法来解决问。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float('inf') total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float('inf') else 0 ``` 以上就是第二十二天的算法训练营的内容。通过这些目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值