LeetCode總結——從2Sum、3Sum、3Sum Closest、4Sum到kSum

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章