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

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

时间:2024-09-17 19:24:46浏览次数:12  
标签:四数 15 nums int List 随想录 元组 right left

第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),它们在集合中被视为相同的元素,因此不会重复添加。

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

标签:四数,15,nums,int,List,随想录,元组,right,left
From: https://blog.csdn.net/weixin_47681529/article/details/142314431

相关文章

  • 代码随想录算法训练营第一天|704二分查找 27移除数组 977.有序数组的平方
    704二分查找给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。示例1:输入:nums=[-1,0,3,5,9,12],target=9输出:4解释:9出现在nums中并且下标为4示例 2:输......
  • 代码随想录算法训练营二天|209. 长度最小的子数组 59.螺旋矩阵II 区间和 开发商购买土
    209.长度最小的子数组太久没做题初始思路只能想到暴力破解,看了一眼提示可能会用到前缀和,能够想到只要建立一个新数组,bi=a0+a1+...+ai即数组a的前缀,这样子序列i到j就可以表示为bj-bi-1,由于数组元素是大于1的,所以b数组必然是递增的,那么在计算子序列的时候,当符合条......
  • 代码随想录算法训练营第三天|203移除链表元素 707设计链表 206反转链表
    203.移除链表元素题意:删除链表中等于给定值val的所有节点。示例1:输入:head=[1,2,6,3,4,5,6],val=6输出:[1,2,3,4,5]示例2:输入:head=[],val=1输出:[]示例3:输入:head=[7,7,7,7],val=7输出:[]链表一直是处理的不太好的数据结构,太久没处理过,第一次做......
  • 代码随想录算法训练营第四天|24两两交换链表中的节点 19删除链表的倒数第N个节点 02.0
    24.两两交换链表中的节点给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。由于今天赶时间,第一眼看完题目没思路就直接看文字解析了,但还是复述一下看完之后自己的理解/想法/思路。这一题感觉对......
  • CF 1527 E
    题目描述我们定义一个数组\(P\)的代价为:\[\sum\limits_{x\inP}last(x)-first(x)\]这里\(first(x),last(x)\)是指\(x\)第一次,最后一次出现的位置。你需要将数组\(A\)分成恰好\(k\)段,求最小总代价。思路令\(dp_{i,j}\)表示已经分了\(i\)段,末尾在\(j\)​的......
  • 代码随想录Day2 | LeetCode 209. 长度最小的子数组、LeetCode 59. 螺旋矩阵 II、KamaC
    LeetCode209.长度最小的子数组子数组是一个连续的,很容易想到滑动窗口classSolution:defminSubArrayLen(self,target:int,nums:List[int])->int:windowSum=0left,right=0,0res=float('inf')whileright<len(nums......
  • macOS Sequoia 15 发布,iPhone 镜像、密码应用程序、窗口平铺更新等带来全新体验
    macOSSequoia15.0(24A335)正式版ISO、IPSW、PKG下载2024年9月17日凌晨1点TimCook领导的Apple今天发布了macOS15Sequoia正式版,这是专为Mac运行的操作系统的最新版本。macOSSequoia是一个免费更新,可以在2018年及更高版本的MacBookPro、2020年及更高......
  • 代码随想录算法训练营,9月17日 | 669. 修剪二叉搜索树,108.将有序数组转换为二叉搜索树,5
    669.修剪二叉搜索树题目链接:669.修剪二叉搜索树文档讲解︰代码随想录(programmercarl.com)视频讲解︰修剪二叉搜索树日期:2024-09-17想法:节点为空返回空,值在中间时,继续递归左右两边,小于时递归右子树,大于时递归左子树Java代码如下:classSolution{publicTreeNodetrimBST......
  • 「杂题乱刷2」CF1527B2
    题目链接CF1527B1(luogu)CF1527B2(luogu)CF1527B1(codeforces)CF1527B2(codeforces)解题思路这篇题解分B1,B2两个部分来讲。B1sol:考虑字符串中\(0\)的数量,设这个值为\(sum\):若\(sum\equiv0\pmod{2}\),且字符串回文时,那么此时,后手可以一直模仿先手的操作,直到字符串含有......
  • P10815 快速读入
    C++标准库提供了强大的输入输出方法。但是,出于设计与安全上的原因,它们的性能不一定能满足算法竞赛的需求,因此在面对巨大的输入输出文件时可能需要考虑优化。注意:通常来说,不必特别在意读写优化。如果习惯cin/cout的话,在文件较大时记得关闭流同步,使用'\n'而不是endl。输入......