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

454. 四数相加II

题目链接 | 解题思路

  1. 遍历第一、二个数组,得到两个数组的所有两数之和以及出现次数,储存在 mapping 中,
    • key:两数之和
    • value:key出现的次数
  2. 遍历第三、四个数组,如果其中两数之和的相反数曾在 mapping 中作为 key 出现过,就在 count 中增加 key 相对应的 mapping value
  3. 最终 count 就统计了答案

因为是独立的四个数组,所以不用去重,而且我们只在乎所有数的和。所以,本质上是把第一、二个数组看作一个数组,第三、四个数组看作一个数组,从而把暴力算法的 O ( n 4 ) O(n^4) O(n4) 降低成 O ( n 2 ) O(n^2) O(n2)

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)

拆分解法 I

class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        sum_12 = dict()
        for a in nums1:
            for b in nums2:
                if a+b not in sum_12:
                    sum_12[a+b] = 1
                else:
                    sum_12[a+b] += 1
        
        sum_34 = dict()
        for c in nums3:
            for d in nums4:
                if -(c+d) in sum_12:
                    if -(c+d) not in sum_34:
                        sum_34[-(c+d)] = 1
                    else:
                        sum_34[-(c+d)] += 1
        
        count = 0
        for sum in sum_34:
            count += sum_12[sum] * sum_34[sum]
        return count

拆分解法 II

class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        sum_12 = dict()
        for a in nums1:
            for b in nums2:
                if a+b not in sum_12:
                    sum_12[a+b] = 1
                else:
                    sum_12[a+b] += 1
        
        count = 0
        for c in nums3:
            for d in nums4:
                if -(c+d) in sum_12:
                    count += sum_12[-(c+d)]
        return count

defaultdict 的炫酷写法

from collections import defaultdict 
class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        record = defaultdict(lambda : 0)
        count = 0

        for a in nums1:
            for b in nums2:
                record[a+b] += 1
        
        for c in nums3:
            for d in nums4:
                if record[-(c+d)]:
                    count += record[-(c+d)]
        return count

383. 赎金信

题目链接 |

重点是只会出现小写字母,所以数值空间范围是有限的。这种明确的条件下,最好是使用数组来构建 hash table。相比之下,mapping 没有数组好。

使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!

数组 - 哈希

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        records = dict()

        for letter in ransomNote:
            if letter not in records:
                records[letter] = 1
            else:
                records[letter] += 1
        
        for letter in magazine:
            if letter in records:
                records[letter] -= 1

        for key in records:
            if records[key] > 0:
                return False
        return True

数组 - 哈希

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        records = [0] * 26

        for letter in ransomNote:
            records[ord(letter) % 26] += 1
        
        for letter in magazine:
            records[ord(letter) % 26] -= 1

        for i in range(len(records)):
            if records[i] > 0:
                return False
        return True

Counter 炫酷解法

from collections import Counter

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        return not Counter(ransomNote) - Counter(magazine)

15. 三数之和(重要!)

题目链接 | 解题思路

这道题看上去和 1.两数之和 / 454.四数相加II 看上去很相似,但些许的不同导致了最终解法的不同。在两数之和中,要返回的是 index,而这道题中返回的是元素值。
更重要的是,之前的题是允许 index/value重复的,但是这次题目要求不能使用重复的 index(最终结果中每个子数组中可以有相同的 value),这点对于 hash table 是致命的,因为使用哈希法进行查询的时候一般并不在乎元素的 index,所以去重会变得非常复杂,同时也失去了使用哈希法的意义。(在自己做的时候,哈希+剪枝也还是超时了)

同时要注意题目中对重复元素的要求:

  • 每个三元子数组中,元素对应的 index 必须不同
  • 每个三元子数组中,元素的 value 可以相同
  • 最终结果中不能有重复的三元子数组(即元素相同,顺序不同的数组)

两数之和 就不能使用双指针法,因为 1.两数之和 要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。
如果 1.两数之和 要求返回的是数值的话,就可以使用双指针法了。

双指针

需要找到符合条件的 a + b + c = 0 a+b+c=0 a+b+c=0。以下的关系中, a a a=nums[i], b b b=nums[left_idx], c c c=nums[right_idx]

  1. 将数组由小到大排序。(这一点是双指针单调性的基础,也彻底告别哈希法)
  2. 从数组的左边开始,定义一个指针 i,遍历整个数组,指代 a a a
    • 如果 nums[i] > 0,由于从小到大排列,代表着肯定无法达成 a + b + c = 0 a+b+c=0 a+b+c=0,直接结束遍历
    • 如果 nums[i] == nums[i-1],则当前的 a a a 已经在之前的 iteration 中被计算过了,当前循环必然会得到和之前 iteration 一模一样的结果,直接跳过当前 iteration(直到发现新的 a a a
      • a a a 的去重,注意是与前一个元素相同时跳过 iteration,而不是更自然的 nums[i] == nums[i+1],因为后者会漏掉直接跳过一个元素第一次出现的情况
  3. 对于给定的 ii 的右侧规定了 left_idxright_idx 的范围,即 b b b c c c 的范围
    • 如果 a + b + c > 0 a+b+c>0 a+b+c>0,则左移 right_idx
    • 如果 a + b + c < 0 a+b+c<0 a+b+c<0,则右移 left_idx
    • 如果 a + b + c = 0 a+b+c=0 a+b+c=0,则获得一个正确的三元子数组
      • 此时,右移 left_idx 直到发现一个新的 b b b,达成对于 b b b 的去重
      • 同样,左移 right_idx 直到发现一个新的 c c c,达成对于 c c c 的去重

在以上算法中,关于i,left_idx, right_idx 的定义决定了 三元子数组内的元素 index 不会相同,同时完整的去重也确保了不会出现重复的三元子数组。

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()

        results = []
        for i in range(len(nums)):
        	# if the first value in i, left_idx, right_idx is positive, no way can achieve sum=0, for-loop ends
            if nums[i] > 0:				
                break
            # avoid the duplicates of "a": if the value of i has occurred, no need to consider it again, otherwise duplicates of "a" occur
            if i > 0 and nums[i] == nums[i - 1]:        # avoid duplicates
                continue
			# uniqueness of "a" has been guaranteed

			# left_idx, right_idx denote the range of the left interval
            left_idx = i + 1
            right_idx = len(nums) - 1
            while (left_idx < right_idx):            
                if nums[i] + nums[left_idx] + nums[right_idx] > 0:		# need to decrease the large value 
                    right_idx -= 1
                elif nums[i] + nums[left_idx] + nums[right_idx] < 0:    # need to increase the small vale
                    left_idx += 1
                else:
                    results.append([nums[i], nums[left_idx], nums[right_idx]])
                    # meet a satisfatory case, move left_idx and right_idx specially
                    curr_left_val, curr_right_val = nums[left_idx], nums[right_idx]
                    while (nums[left_idx] == curr_left_val and left_idx < right_idx):			# move left_idx to a new value 
                        left_idx += 1
                    while (nums[right_idx] == curr_right_val and left_idx < right_idx):			# move right_idx to a new value
                        right_idx -= 1
        return results

18. 四数之和

题目链接 | 解题思路

思路与上一题基本相同,依然是固定两个遍历指针,在剩余区间内依靠双指针找到剩下符合条件的解。
唯一变化的是剪枝的条件发生了变化:由于不清楚 target 的正负,剪枝的条件变得更加苛刻了。

  • 时间复杂度: O ( n 3 ) O(n^3) O(n3)
  • 空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()

        results = []
        for i in range(len(nums)):
            if nums[i] > target and nums[i] > 0 and target > 0:		# cut the case
                break
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            for j in range(i+1, len(nums)):
                if j < len(nums) - 1 and nums[j] == nums[j + 1]:
                    continue
                left_idx = i + 1
                right_idx = j - 1
                while (left_idx < right_idx):
                    
                    if nums[i] + nums[j] + nums[left_idx] + nums[right_idx] > target:
                        right_idx -= 1
                    elif nums[i] + nums[j] + nums[left_idx] + nums[right_idx] < target:
                        left_idx += 1
                    else:
                        results.append([nums[i], nums[j], nums[left_idx], nums[right_idx]])
                        curr_left_val, curr_right_val = nums[left_idx], nums[right_idx]
                        while (nums[left_idx] == curr_left_val and left_idx < right_idx):
                            left_idx += 1
                        while (nums[right_idx] == curr_right_val and left_idx < right_idx):
                            right_idx -= 1
        return results
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值