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]相關題目彙總/分析/總結