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]相关题目汇总/分析/总结

发布了143 篇原创文章 · 获赞 161 · 访问量 29万+

猜你喜欢

转载自blog.csdn.net/vivian_ll/article/details/104268135
今日推荐