python数列求和例题_leetcode-python-数组中求和问题(三)

专题概述

本专题将讲解的题目为leetcode中1, 15, 18, 454四道题,两道相关题目16和167作为练习题。主要介绍哈希表和指针两种方法来解决该类问题,从两个数之和引申到三个数之和,再从四个数之和的问题上思考如何构建出一种通用的代码(可以解决N个数之和)。相信通过这四道题的讲解,当再次遇到类似问题,一定可以顺利的解决。

目录

代码相关所有代码在leetcode英文网站上都通过了测试。

github地址,欢迎star。本专题代码在code/sum-in-array中。dyq666/leetcode-python​github.comv2-9a0db56821d6df1be4d2e35b0254b52f_ipico.jpg如果觉得本文对你有帮助,可以收藏点赞,若文中有问题可在评论区留言

本文的内容

通过018-4Sum问题来总结数组求和的两种常用方法,将其推广到NSum问题。阅读前需要掌握如何将问题转化为两个数相加问题,重复的内容在本文中不在叙述。

018-四数之和给定一个包含 n个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素a,b,c 和d ,使得 a+b+c+d 的值与 target 相等?找出所有满足条件且不重复的四元组。

实例:给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。

满足要求的四元组集合为:

[

[-1, 0, 0, 1],

[-2, -1, 1, 2],

[-2, 0, 0, 2]

1. 延续三个数求和的思路O(n^3)

对于四个数来说我们可以将问题看作是n*n个两数相加问题

每一层循环都会涉及到之前的优化问题,你可以按照之前每层循环都是改变target的角度来理解,为什么最外层的循环也要使用这个优化。

延续上一文最后提到的优化问题,当需要四个数求和时(也就是本题最外层循环),还剩三个数就停止,当需要三个数求和时(本题第二层循环),还剩两个数就停止。

nums.sort()

len_nums = len(nums)

res = set()

# 外层由遍历一轮变成遍历两轮

for i in range(len_nums - 3): # 新

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

continue

for j in range(i+1, len_nums - 2):

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

continue

seen_set = set()

# 一样的双数的HashSet

for v_k in nums[j+1:]:

search = target - nums[i] - nums[j] - v_k

if search in seen_set:

res.add((nums[i], nums[j], search, v_k))

else:

seen_set.add(v_k)

return list(res)

2. 用双指针这里不再讲述本方法了,相比你已经可以自行完成了,我们将主要精力放在下面的内容

推广到NSum问题

问题概述

主要需要解决三部分内容,双指针的通用性代码,哈希的通用性代码,以及嵌套for循环问题。

1.前置问题如果你不太了解为什么要考虑这部分中的问题,先往下看。

2. 问题一:最终的结果都来自于双指针/哈希部分,如果将这两部分都封装成函数,我们就需要考虑如何在其他函数中得到这个值,这里我们采用了类实例属性的办法,初始化时保存一个实例属性res,之后得到的值都将存储在此,返回结果是返回这个实力属性即可。当然也可以使用返回值的方式,只不过这种方式需要使用python中集合的并集(python的语法 seta | setb)。

3.问题二:在上面提到的封装中,由于问题转换成了两个数之和,所以我们只能在这个函数中得到两个值,但是实际返回的tuple可能是大于2的,所以我们就需要将之前的值传递进来,具体传递过程在for嵌套中会提到,现在你只需知道这两个函数可以接收到之前所有的值即可。

2.双指针代码大部分代码都可以直接复制3Sum中的双指针部分

区别在于,我们需要知道最左边的起始位置(left_start),以及之前所有循环对应的值(saved_items)

当找到符合条件的tuple时,我们就需要存储这个tuple,在上面的问题一种我们提到了如何存储这个tuple,需要注意的是saved_items将传入一个list,我们使用*来拆包(python unpack语法,具体需要查阅相关资料)

def _find_sum_by_pointer(self, nums, left_start, target, saved_items):

"""1. 使用双指针来寻找值2. 根据要查找的值, 将所有可能的组合存到self.res3. 参数说明 nums-原始的数组, 起始点, 查找的目标, 要存储的值"""

left, right = left_start, len(nums) - 1

while left < right:

two_sum = nums[left] + nums[right]

if two_sum < target:

left += 1

elif two_sum > target:

right -= 1

else: # 找到了

self.res.add((*saved_items, 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

3.哈希代码这部分就不详细叙述了只是将上面的讲到的和3Sum代码结合一下就完成了

def _find_sum_by_hash(self, nums, left_start, target, saved_items):

"""1. 使用hash的方法2. 根据要查找的值, 将所有可能的组合存到self.res3. 参数说明 nums-原始数组的一部, 起始点, 查找的目标, 要存储的值"""

seen_set = set()

for num in nums[left_start:]:

# 如果找到目标就存储到结果集合(res),没找到就将值加入已经遍历过的集合(seen_set)

search = target - num

if search in seen_set:

self.res.add((*saved_items, search, num))

else:

seen_set.add(num)

4.嵌套循环问题如何嵌套执行for循环呢? 使用递归,通过start来指定起始位置,然后递归for循环,在指定一个条件结束(例子中是start=5时)

def nested_for(start):

if start == 5:

return

for i in range(start, 10):

print(i)

nested_for(start + 1)

2. NSum中的嵌套条件使用参数N来标识还剩几层for循环,参数start为当前循环起始索引,nums是整个排序后的数组。

在上面的基础上我们增加了之前提到的两个优化。优化1是遍历数组的长度,要记住这里的N是还剩几次for循环不是之前的要求几个数相加(举例: 4个数相加需要外面需要两层循环),可以举几个例子就能知道(len(nums) - (N+1))的逻辑了。优化二是之前提到的重复问题。

当N=0时就要去使用双指针或哈希了。

def _for_nested(self, N, nums, start):

# 如果循环次数为0, 则使用指针或者哈希查找target

if N == 0:

// Todo 去使用两个方法其中之一去寻找符合题意的tuple

else:

for i in range(start, len(nums) - (N + 1)):

# 从第二个开始, 如果值与前一个值相同则continue, 因为相同的值往下面进行循环是无意义的

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

continue

self._for_nested(N - 1, nums, i + 1)

3. 完善for嵌套主要是解决如何向哈希/双指针函数中传递适当的参数

就上面的例子来看目前由两个参数未解决:一是target,二是saved_items,而target又是由原始的target-sum(saved_items)得到的,所以只要解决了saved_items问题就解决了

首先要明白传值中引用的概念,也就是如果在外面调用_for_nested函数,向saved_items中传递了一个list,那么函数中会一直使用同一个list。

在向saved_items传递了[None] * (N)(也就是传递一个跟循环次数同等大小的列表),在每次循环中都将对应的位置保存当前循环的索引对应的值,需要注意的是这里我们赋值是按大小逆序赋值的,所以在传给双指针/哈希函数时需要反转列表。

def _for_nested(self, N, nums, start, target, saved_items):

# 如果循环次数为0, 则使用指针或者哈希查找target

if N == 0:

# 获取所有前面循环的索引对应的值

target = target - sum(saved_items)

# 两种查找target的方式

self._find_sum_by_pointer(nums, start, target, list(reversed(saved_items)))

# self._find_sum_by_hash(nums, start, target, list(reversed(saved_items)))

else:

for i in range(start, len(nums) - (N + 1)):

# 从第二个开始, 如果值与前一个值相同则continue, 因为相同的值往下面进行循环是无意义的

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

continue

# 在对应的位置存储当前循环对应的索引

saved_items[N - 1] = nums[i] # 新

self._for_nested(N - 1, nums, i + 1, target, saved_items)

4.在增加一个优化其实到目前为止代码已经能使用了,只需要在外层调用上面的嵌套循环函数,将N(for循环的个数,比如求3个数的和,N=1,4个数,N=2,N=个数-2),排序后的nums,start=0,target=target,saved_items=[None]*N(在上面的例子中已经提到了)。然后再返回self.res就可以得到答案了

增加一个_start函数,其中N_sum是求和的个数。步骤为数组排序, 考虑四个不符合条件的情况(原题目特别声明了并不会出现这些情况,但是处于完整性,我们还是加入了这些错误情况的考虑,具体可以阅读下面的注释),然后再调用for嵌套函数。

def _start(self, N_sum, nums, target):

# 数组排序

nums.sort()

# 数组的数量小于加数的个数, 加数的个数小于2, 目标值比数组中最小的N倍还小, 目标值比数组中最大的N倍还大。这四种都是无效的

if len(nums) < N_sum or N_sum < 2 or target < nums[0] * N_sum or target > nums[-1] * N_sum:

return

# 4个数的合就需要外层两层循环(类推5需要3层..), 还需要初始化一个同样大小的存储每层循环中索引

self._for_nested(N_sum - 2, nums, 0, target, [None] * (N_sum - 2))

总结

1. O(n^(k-1)), k是要加数的个数

2. 只适用于返回所有查找到的的值(之前介绍的4Sum和3Sum都行,但是2Sum不行)

3. 两种方式指针和哈希的比较, 在同等条件下(比如都需要排序数组), 指针方式优于哈希, 因为使用哈希时需要不停的创建一个哈希Set。(不一定准确,但是在leetcode上一般指针都会快一些)

4. 下一章我们将介绍下这类问题的变形

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值