Python哈希表一招解決nSum問題

自古各門各家武學都存在套路,正所謂以不變應萬變,就在於臨戰之時,能夠一招制敵。有的招數可能出奇制勝,可是最穩定的方式必定是屢次訓練的套路,它不必定能讓你解決全部的問題,可是它足以讓你輕鬆應對一類問題。html

nSum的問題,主要存在大量重複的數使得若是在數組中遍歷每一個數,再比較查詢結果,時間複雜度會超過題目的要求。咱們能夠採用哈希表的方式,加速查詢的過程,同時對遍歷的過程,對相同的數或者不知足條件的數適當的跳過,能夠有效的提高效率,經過測試用例。數組

那麼先從最簡單的兩數之和講起。app

一、 Leetcode 1 兩數之和測試

最暴力的方式是從頭至尾枚舉 nums 中的每個數,而後再看是否在數組中存在 j 使得 nums[j]  == target - nums[i] 且 j != i,這樣的方式,遍歷是 O(n) 複雜度,每次循環在數組中查詢也是 O(n) 複雜度,總的時間複雜度達到了 O(n^2)。代碼和運行時間以下:優化

 1 class Solution:
 2     def twoSum(self, nums: List[int], target: int) -> List[int]:
 3         result = []
 4         for i in range(len(nums)):
 5             if nums[i] in nums and target-nums[i] in nums:
 6                 j = nums.index(target-nums[i])
 7                 if i!=j:
 8                     result.append(i)
 9                     result.append(j)
10                     break
11         return result

這樣作顯然太過耗時,循環若是不作改變的話(作改變的話能夠用雙指針法,這裏不作過多介紹),那麼考慮在查找過程當中進行加速。咱們注意到哈希表中查找元素的時間是 O(1),所以能夠把在數組中查找改成在哈希表中查找。對於這題而言,只要找到了答案就能夠返回,不須要找出全部的解,那麼能夠邊遍歷邊向哈希表中添加元素。添加前,查詢是否有知足條件的解,若是知足條件,return結果就能夠。spa

1 class Solution:
2     def twoSum(self, nums: List[int], target: int) -> List[int]:
3         dic = {}
4         for i,num in enumerate(nums):
5             tmp = target - num # a + b = target, a = num, b = target - num
6             if tmp in dic:     # 哈希表中查詢是否有解
7                 return [i, dic[tmp]]
8             dic[num] = i       # 沒有解的話就存下當前的數和位置

運行結果以下:3d

最差的狀況下,在線性時間內就能夠解決問題指針

接下來考慮複雜一點的問題code

二、 Leetcode 15 三數之和htm

首先題目要求解集裏面不包含重複的元素,那麼按照必定的規律找答案,就能夠獲得不重複的解。能夠想到的方法是先進行排序,這樣就能夠有規律的尋找了。排序之後,每一個數也可能有多個重複的,假如每一個解裏不能包含相同的數字,那麼簡單的在循環里加上 

1 if i > index and nums[i] == nums[i-1]:
2     continue

其中 index 是循環開始的值, 而且下一層循環從 i + 1開始,就能夠保證無重複了。這樣的去重方式能夠參考個人另外一篇文章講到的第三類問題, http://www.javashuo.com/article/p-qrixdqqf-mr.html 

然而這道題則是每一個解裏能夠包含相同的數字,好比 [-1, -1, 2] 和 [0, 0, 0] 均可以獲得和爲0,這時候去重就能夠用到Python中計數哈希表, Counter。先統計每一個數字出現的次數,再對鍵值進行排序,每層循環裏判斷剩餘的數字是否夠當前的變量選擇。

本題解法參考 https://leetcode-cn.com/problems/3sum/solution/ji-shu-zi-dian-jian-zhi-you-hua-fei-pai-xu-shuang-/

代碼以下:

 1 from collections import Counter
 2 class Solution:
 3     def threeSum(self, nums: List[int]) -> List[List[int]]:
 4         res = []
 5         dic = Counter(nums)  # Counter能夠統計數組每一個元素的個數
 6         hash_nums = sorted(dic.keys()) #對鍵值進行排序
 7         for i, a in enumerate(hash_nums):
 8             dic[a] -= 1   # a已經取走了一個數字,字典裏對應位置 -1
 9             for b in hash_nums[i:]:
10                 if dic[b] < 1: # b從i開始遍歷,i也是當前a的位置,若是減去1之後b不夠選了,跳過這一個位置
11                     continue
12                 c = -(a + b)
13                 if c < b:   #有序的查找,若是c都比b小,以後b再增大,確定c更小,那麼就跳出,防止重複
14                     break
15                 # 再判斷c和b的關係,若是相等,那就須要dic[c]至少爲2,纔夠選,若是不等,只要有,就夠選了
16                 if (c > b and dic[c] > 0) or (c == b and dic[c] > 1):
17                     res.append([a, b, c])
18         return res

時間複雜度 O(n^2)

空間複雜度 O(n)

提交結果:

有了這樣的經驗之後,咱們能夠用已有的套路看更復雜的四數之和

三、Leetcode 18  四數之和

最外層循環遍歷到什麼位置,就在對應位置上減1,接下來內層循環裏也把選擇的數減1,方便後面進行判斷,只要不夠選了,就continue跳過這一次循環,若是最終的d比c還大,依舊break掉,和三數之和的差異在於,第二個數選擇的時候要減去1,最內層循環結束之後還要加回1,由於以後最外層的a也會再遍歷到這個位置。

代碼以下:

 1 from collections import Counter
 2 class Solution:
 3     def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
 4         res = []
 5         dic = Counter(nums) #對每一個數出現的次數進行統計
 6         arr = sorted(dic.keys())  #排序鍵值
 7         for i, a in enumerate(arr):
 8             dic[a] -= 1 #a用掉了一次,並且a的位置以後不會再遍歷到了,不須要加回
 9             for j, b in enumerate(arr[i:]):  #從arr[i]開始找b的值
10                 if dic[b] < 1: #b可能等於a,判斷一下,若是dic[b]不夠1個,跳過此次循環
11                     continue
12                 dic[b] -= 1
13                 for c in arr[i+j:]:  #從arr[i+j]開始找c的值,注意上一層循環枚舉j之後,須要再加最外層的i
14                     if dic[c] < 1: #同上層循環b的判斷
15                         continue
16                     d = target - (a + b + c)  
17                     if d < c:   #由於是非遞減順序,若是d小於c,就直接跳出,這樣就能夠避免重複
18                         break
19                     if (d == c and dic[d] > 1) or (d > c and dic[d] > 0):
20                         res.append([a, b, c, d])
21                 dic[b] += 1 #b如今所處的位置,以後a還會遍歷到,所以須要加回1
22         return res

時間複雜度 O(n^3)

空間複雜度 O(n)

提交結果:

以此能夠類推到更多數字的和。最外層循環每選到一個位置之後,都減去1,內層的循環也選到一個位置減去1,在更內層的循環結束之後加回1就能夠。最內層轉化爲2數之間的大小關係的比較和查詢哈希表是否有知足條件的值。

最後看一個變種問題

Leetcode 1577   數的平方等於兩數乘積的方法數

這一題若是暴力求解,必然超時,那麼就須要一些優化策略。兩個數組裏可能會存在不少相同的數,它們僅僅是位置不一樣,找到的 j, k的結果卻同樣,好比 nums1 = [1,1,1,1],nums2 = [1,1,1,1,1,1],1中每一個數的平方,都等於2中任意兩個不一樣位置的數的乘積,咱們沒有必要對每一個相同的 nums1 中的數都找一遍 nums2 中全部的數,這就又回到了 nSums 問題,能夠想到的去重的方式是哈希表。

這裏的技巧在於若是對於每一個平方數去找是否存在兩個數和它相等,每一個平方數遍歷的時間是 O(n), 再找兩個數,若是要達到 O(m) 複雜度,就應該考慮雙指針的方式,而後須要各類比較,代碼相對複雜,容易出錯。若是換個思路,從右向左找,對於每一個數字的乘積,都在哈希表裏找是否存在相應的平方數,那麼時間複雜度就是兩次遍歷數組的時間複雜度 × 哈希表查找的時間複雜度,因爲哈希表查找是 O(1),最終等於兩次遍歷數組的時間複雜度。

代碼以下:

 1 from collections import Counter
 2 class Solution:
 3     def numTriplets(self, nums1: List[int], nums2: List[int]) -> int:
 4         square1 = Counter([i*i for i in nums1])
 5         square2 = Counter([i*i for i in nums2])
 6         res = 0
 7         for i in range(len(nums2)):
 8             for j in range(i+1, len(nums2)):
 9                 tmp = nums2[i] * nums2[j] # 能夠用tmp存一下兩數之積,避免後面字典查詢的時候再次重複計算鍵值
10                 if tmp in square1:
11                     res += square1[tmp]
12         for i in range(len(nums1)):
13             for j in range(i+1, len(nums1)):
14                 tmp = nums1[i] * nums1[j] # 同理
15                 if tmp in square2:
16                     res += square2[tmp]
17         return res

時間複雜度 O(n^2 + m^2)  其中 m,n 是 nums1 和 nums2 的數組長度

空間複雜度 O(n+m)

代碼執行結果以下:

之因此說在每次循環中要用 tmp 存一下兩數的乘積,由於隨着數據量的增長,重複計算兩數乘積的代價也是至關大的,若是兩次都直接用

1 if nums2[i] * nums2[j] in square1:
2     res += square1[nums2[i] * nums2[j]]

運行時間將會明顯提高,提交結果以下:

相關文章
相關標籤/搜索