leetcode求和问题描述(K sum problem):
给你一组N个数字(nums), 然后给你一个常数(target) ,我们的目标是在这一堆数里面找到K个数字,使得这K个数字的和等于target。
注意事项(constraints):
注意这一组数字可能有重复项:比如 1 1 2 3 , 求3sum, 然后 target = 6, 你搜的时候可能会得到 两组1 2 3, 1 2 3,1 来自第一个1或者第二个1, 但是结果其实只有一组,所以最后结果要去重。
2Sum
LeetCode1
方法一: 暴力法。枚举所有的K-subset, 那么这样的复杂度就是 从N选出K个,复杂度是O(N^K)。
def twoSum(nums, target):
# 暴力法 二重循环 7228 ms, 3.54% O(n^2)
for i in range(0,len(nums)):
for j in range(i+1,len(nums)): # 注意为i+1 而非i,不然两个index则相同
if nums[i]+nums[j] == target:
print(i,j)
index = [i,j]
return index
方法二: 头尾指针法。先排序,然后利用头尾指针找到两个数使得他们的和等于target。2sum是最简单的情况,这是个经典方法,在3Sum里给出代码,这里不赘述了。
算法复杂度是O(N log N) 因为排序用了N log N以及头尾指针的搜索是线性的,所以总体是O(N log N)。
方法三:Hash法。线性解法,使用hashmap, 则查找某个值存在不存在就是常数时间,那么给定一个target, 只要线性扫描, 对每一个num判断target – num存在不存在就可以了。这是最快的方法,复杂度O(N)。
注意:这个算法对有重复元素的序列也是适用的。比如 2 3 3 4 那么hashtable可以使 hash(2) = 1; hash(3) = 1, hash(4) =1其他都是0, 那么check的时候,扫到两次3都是check sum – 3在不在hashtable中,注意最后返回所有符合的pair的时候也还是要去重。
def twoSum(nums, target):
# 建立字典 不一下子全加进去 一个个加一个个找
if len(nums) <= 1:
return False
buff_dict = {}
for i in range(len(nums)):
if nums[i] in buff_dict:
return [buff_dict[nums[i]], i]
else:
buff_dict[target - nums[i]] = i
3sum 其实也有O(N^2)的类似hash算法,这点和之前是没有提高的,但是4sum就会有更快的一个算法。
3Sum
LeetCode15 (LeetCode上这一题的target为0)
思路:先取出一个数,那么我只要在剩下的数字里面找到两个数字使得他们的和等于(target – 那个取出的数)就可以了吧。所以3sum就退化成了2sum, 取出一个数字,这样的数字有N个,所以3sum的算法复杂度就是O(N^2 ), 注意这里复杂度是N平方,因为你排序只需要排一次,后面的工作都是取出一个数字,然后找剩下的两个数字,找两个数字是2sum用头尾指针线性扫,这里很容易错误的将复杂度算成O(N^2log N),这个是不对的。
方法:头尾指针法
def threeSum(nums):
# 二重循环 先排序 最外层循环遍历第一个数,后面两个数j,k在每次最外层循环开始时,取i+1和len(nums)-1,就是i之后的一头一尾。nums[i]+nums[j]+nums[k]如果小于0那就是nums[j]不够大,我们把j后移一个,如果大于0那就是nums[k]太大了,我们把k前移一个。
res = []
nums.sort()
for i in range(len(nums) - 2):
# 考虑过的i就不用再考虑了
if i > 0 and nums[i] == nums[i - 1]:
continue
# 第二第三个数:一头一尾
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0:
l += 1
elif s > 0:
r -= 1
# 分支应该写清楚
else:
res.append([nums[i], nums[l], nums[r]])
# 考虑过的l就不要考虑了
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
return res
3Sum closest
LeetCode16
方法:头尾指针法 方法和3Sum类似,但是要多维护一个最小的diff, 保存和target最近的值。算法的复杂度是O(n^2)。
注意: 这道题使用Two Sum的第一种方法并不合适, 因为当出现元素重复时用哈希表就不是很方便了。
def threeSumClosest(nums, target):
# 二重循环 先排序 最外层循环遍历第一个数,后面两个数j,k在每次最外层循环开始时,取i+1和len(nums)-1,就是i之后的一头一尾。nums[i]+nums[j]+nums[k]如果小于目标那就是nums[j]不够大,我们把j后移一个,如果大于0那就是nums[k]太大了,我们把k前移一个。
res = 0
nums.sort()
diff = float('inf')
for i in range(len(nums) - 2):
# 考虑过的i就不用再考虑了
if i > 0 and nums[i] == nums[i - 1]:
continue
# 第二第三个数:一头一尾
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
temp_diff = abs(s - target)
if temp_diff < diff:
res = s
diff = temp_diff
if s < target:
l += 1
elif s > target:
r -= 1
# 分支应该写清楚
else:
return s
return res
4Sum
LeetCode18
题目描述:给定一个有n个整数的数组S和目标值target,找到其中所有由四个数a、b、c、d组成,使得a + b + c + d = target 的四元组。
思路:4Sum可以退化成3Sum,做法与3Sum一样。相当于在3Sum的基础上。再嵌套一层for循环。
方法一:头尾指针法。3Sum的推广。与 3Sum 的思路一样,先排序再左右夹逼,时间复杂度O(n^3),空间O(1)。
def fourSum(nums, target):
# 三重循环 先排序 最外层循环遍历第一个数,后面两个数j,k在每次最外层循环开始时,取i+1和len(nums)-1,就是i之后的一头一尾。nums[i]+nums[j]+nums[k]如果小于0那就是nums[j]不够大,我们把j后移一个,如果大于0那就是nums[k]太大了,我们把k前移一个。
res = []
nums.sort()
for j in range(len(nums)):
for i in range(j+1,len(nums) - 2):
# 第二第三个数:一头一尾
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[j] + nums[i] + nums[l] + nums[r]
if s < target:
l += 1
elif s > target:
r -= 1
# 分支应该写清楚
else:
if [nums[j], nums[i], nums[l], nums[r]] not in res:
res.append([nums[j], nums[i], nums[l], nums[r]])
# 考虑过的l就不要考虑了
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
return res
这里注意for循环的边界:先整体排一次序,然后枚举第三个数字的时候不需要重复, 比如排好序以后的数字是 a b c d e f, 那么第一次枚举a, 在剩下的b c d e f中进行2 sum, 完了以后第二次枚举b, 只需要在 c d e f中进行2sum好了,而不是在a c d e f中进行2sum。
方法二:Hash法。这里的hash法和2Sum不同,先用 hashmap 缓存两个数的和,那么接下来求4sum就变成了在所有的pair value中求 2sum,这个就成了线性算法了。复杂度平均O(n^3) ,最坏O(n^4 ),空间复杂度O(n^2)。
因为本质上我们是最外层两个循环之后,找是否有target-now的值,我们可以事先做好预处理,即空间换时间,先循环两次,找出两个数所有可能的和,存到map里。这两等于是两个O(n2)的时间复杂度相加和,所以最后时间复杂度为O(n2),但是此时需要有一个判重的问题,所以最糟糕情况下其时间复杂度为O(n4)。
def fourSum(nums, target):
# 建立一个字典dict,字典的key值为数组中每两个元素的和,两重循环,平均O(n^2 ),最坏O(n^4 )
numLen, res, num_dict = len(nums), set(), {}
if numLen < 4:
return []
nums.sort() # 不能省略
for p in range(numLen): # 存储hash表
for q in range(p + 1, numLen):
if nums[p] + nums[q] not in num_dict:
num_dict[nums[p] + nums[q]] = [(p, q)]
else:
num_dict[nums[p] + nums[q]].append((p, q))
print(num_dict)
for i in range(numLen):
for j in range(i + 1, numLen - 2):
T = target - nums[i] - nums[j]
if T in num_dict:
for k in num_dict[T]:
if k[0] > j: res.add((nums[i], nums[j], nums[k[0]], nums[k[1]]))
return [list(i) for i in res]
kSum
K-sum一步一步退化,最后也就是解决一个2sum的问题。双指针法,方法就是先排序,然后利用头尾指针找到两个数使得他们的和等于target,其他Ksum都是同样的思路,只不过要固定前K-2个(利用循环)K sum的复杂度是O(n^(K-1))。
(这个界好像是最好的界了,也就是K-sum问题最好也就能做到O(n^(K-1))复杂度,之前有看到过有人说可以严格数学证明,这里就不深入研究了。)
递归实现:
def kSum(k, nums, target):
def findNSum(nums, l, r, target, N, result, results):
if N < 2 or r - l + 1 < N or target < nums[l] * N or target > nums[r] * N:
return
if N == 2:
while l < r:
value = nums[l] + nums[r]
if value == target:
results.append(result + [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 value > target:
r -= 1
else:
l += 1
else: # Reduce to N-1
for i in range(l, r + 1):
if i > l and nums[i] == nums[i - 1]:
continue
findNSum(nums, i + 1, r, target - nums[i], N - 1, result + [nums[i]], results)
results = []
nums.sort()
findNSum(nums, 0, len(nums) - 1, target, k, [], results)
return results
注意:递归时不要重复排序
参考网址:
【Ksum】求和问题总结(leetcode 2Sum, 3Sum, 4Sum, K Sum)
[Leetcode][python]4Sum
[Leetcode][求和问题2Sum/3Sum/4Sum/KSum]相关题目汇总/分析/总结