Leetcode精选50题-Day04

016 最接近的三数之和

1. 题目描述

给定一个包括 n n n 个整数的数组 n u m s nums nums 和 一个目标值 t a r g e t target target。找出 n u m s nums nums 中的三个整数,使得它们的和与 t a r g e t target target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

在这里插入图片描述

2. 思路&代码

首先考虑枚举第一个元素 a a a,对于剩下的两个元素 b b b c c c,我们希望它们的和最接近 t a r g e t − a target−a targeta。对于 b b b c c c,如果它们在原数组中枚举的范围(既包括下标的范围,也包括元素值的范围)没有任何规律可言,那么我们还是只能使用两重循环来枚举所有的可能情况。因此,我们可以考虑对整个数组进行升序排序,这样一来:

假设数组的长度为 n n n,我们先枚举 a a a,它在数组中的位置为 i i i

为了防止重复枚举,我们在位置 [ i + 1 , n ) [i+1,n) [i+1,n) 的范围内枚举 b b b c c c

当我们知道了 b b b c c c 可以枚举的下标范围,并且知道这一范围对应的数组元素是有序(升序)的,那么我们是否可以对枚举的过程进行优化呢?

答案是可以的。借助双指针,我们就可以对枚举的过程进行优化。我们用 p b p_b pb p c p_c pc 分别表示指向 b b b c c c 的指针,初始时, p b p_b pb 指向位置 i + 1 i+1 i+1,即左边界; p c p_c pc 指向位置 n − 1 n−1 n1,即右边界。在每一步枚举的过程中,我们用 a + b + c a+b+c a+b+c 来更新答案,并且:

如果 a + b + c ≥ t a r g e t a+b+c≥target a+b+ctarget,那么就将 p c p_c pc 向左移动一个位置;
如果 a + b + c < t a r g e t a+b+c<target a+b+c<target,那么就将 p b p_b pb 向右移动一个位置。

这是为什么呢?我们对 a + b + c ≥ t a r g e t a+b+c≥target a+b+ctarget 的情况进行一个详细的分析:

如果 a + b + c ≥ t a r g e t a+b+c≥target a+b+ctarget,并且我们知道 p b p_b pb p c p_c pc 这个范围内的所有数是按照升序排序的,那么如果 p c p_c pc
不变而 p b p_b pb 向右移动,那么 a + b + c a+b+c a+b+c 的值就会不断地增加,显然就不会成为最接近
t a r g e t target target的值了。因此,我们可以知道在固定了 p c p_c pc 的情况下,此时的 p b p_b pb 就可以得到一个最接近
\textit{target}target 的值,那么我们以后就不用再考虑 p c p_c pc 了,就可以将 p c p_c pc 向左移动一个位置。

同样地,在 a + b + c < t a r g e t a+b+c<target a+b+c<target 时:

如果 a + b + c < t a r g e t a+b+c<target a+b+c<target,并且我们知道 p b p_b pb p c p_c pc 这个范围内的所有数是按照升序排序的,那么如果 p b p_b pb
不变而 p c p_c pc 向左移动,那么 a + b + c a+b+c a+b+c 的值就会不断地减小,显然就不会成为最接近 t a r g e t target target
的值了。因此,我们可以知道在固定了 p b p_b pb 的情况下,此时的 p c p_c pc 就可以得到一个最接近 t a r g e t target target
的值,那么我们以后就不用再考虑 p b p_b pb了,就可以将 p b p_b pb 向右移动一个位置。

实际上, p b p_b pb p c p_c pc 就表示了我们当前可以选择的数的范围,而每一次枚举的过程中,我们尝试边界上的两个元素,根据它们与 t a r g e t target target 的值的关系,选择「抛弃」左边界的元素还是右边界的元素,从而减少了枚举的范围。

小优化

本题也有一些可以减少运行时间(但不会减少时间复杂度)的小优化。当我们枚举到恰好等于 t a r g e t target target a + b + c a+b+c a+b+c 时,可以直接返回 t a r g e t target target 作为答案,因为不会有再比这个更接近的值了。

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        n = len(nums)
        best = 10**7
        
        # 根据差值的绝对值来更新答案
        def update(cur):
            nonlocal best
            if abs(cur - target) < abs(best - target):
                best = cur
        
        # 枚举 a
        for i in range(n):
            # 保证和上一次枚举的元素不相等
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            # 使用双指针枚举 b 和 c
            j, k = i + 1, n - 1
            while j < k:
                s = nums[i] + nums[j] + nums[k]
                # 如果和为 target 直接返回答案
                if s == target:
                    return target
                update(s)
                if s > target:
                    # 如果和大于 target,移动 c 对应的指针
                    k0 = k - 1
                    # 移动到下一个不相等的元素
                    while j < k0 and nums[k0] == nums[k]:
                        k0 -= 1
                    k = k0
                else:
                    # 如果和小于 target,移动 b 对应的指针
                    j0 = j + 1
                    # 移动到下一个不相等的元素
                    while j0 < k and nums[j0] == nums[j]:
                        j0 += 1
                    j = j0

        return best

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/3sum-closest/solution/zui-jie-jin-de-san-shu-zhi-he-by-leetcode-solution/

3. 复杂度分析

时间复杂度 O ( N 2 ) O(N^2) O(N2),其中 N N N 是数组 n u m s nums nums 的长度。我们首先需要 O ( N log ⁡ N ) O(N \log N) O(NlogN) 的时间对数组进行排序,随后在枚举的过程中,使用一重循环 O ( N ) O(N) O(N) 枚举 a a a,双指针 O ( N ) O(N) O(N) 枚举 b b b c c c,故一共是 O ( N 2 ) O(N^2) O(N2)

空间复杂度 O ( log ⁡ N ) O(\log N) O(logN)。排序需要使用 O ( log ⁡ N ) O(\log N) O(logN)的空间。然而我们修改了输入的数组 n u m s nums nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了 n u m s nums nums 的副本并进行排序,此时空间复杂度为 O ( N ) O(N) O(N)

020 有效的括号

1. 题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 注意空字符串可被认为是有效字符串。

在这里插入图片描述

2. 思路&代码

判断括号的有效性可以使用「栈」这一数据结构来解决。

我们对给定的字符串 s s s 进行遍历,当我们遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。

当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s s s 无效,返回 F a l s e False False。为了快速判断括号的类型,我们可以使用哈希映射(HashMap)存储每一种括号。哈希映射的键为右括号,值为相同类型的左括号。

在遍历结束后,如果栈中没有左括号,说明我们将字符串 s s s 中的所有左括号闭合,返回 T r u e True True,否则返回 F a l s e False False

注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 F a l s e False False,省去后续的遍历判断过程。

class Solution:
    def isValid(self, s: str) -> bool:
        if len(s) % 2 == 1:
            return False
        
        pairs = {
            ")": "(",
            "]": "[",
            "}": "{",
        }
        stack = list()
        for ch in s:
            if ch in pairs:
                if not stack or stack[-1] != pairs[ch]:
                    return False
                stack.pop()
            else:
                stack.append(ch)
        
        return not stack

3. 复杂度分析

时间复杂度:O(n)O(n),其中 n n n 是字符串 s s s 的长度。

空间复杂度: O ( n + ∣ Σ ∣ ) O(n + |\Sigma|) O(n+Σ),其中 Σ \Sigma Σ 表示字符集,本题中字符串只包含 6 6 6 种括号, ∣ Σ ∣ = 6 |\Sigma| = 6 Σ=6。栈中的字符数量为 O ( n ) O(n) O(n),而哈希映射使用的空间为 O ( ∣ Σ ∣ ) O(|\Sigma|) O(Σ),相加即可得到总空间复杂度。

021 合并两个有序链表

1. 题目描述

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
在这里插入图片描述

2. 思路&代码

2.1 递归解法

思路
可以如下递归地定义两个链表里的 m e r g e merge merge 操作(忽略边界情况,比如空链表等):

{ l i s t 1 [ 0 ] + m e r g e ( l i s t 1 [ 1 : ] , l i s t 2 ) l i s t 1 [ 0 ] < l i s t 2 [ 0 ] l i s t 2 [ 0 ] + m e r g e ( l i s t 1 , l i s t 2 [ 1 : ] ) o t h e r w i s e \left\{ \begin{array}{ll} list1[0] + merge(list1[1:], list2) & list1[0] < list2[0] \\ list2[0] + merge(list1, list2[1:]) & otherwise \end{array} \right. {list1[0]+merge(list1[1:],list2)list2[0]+merge(list1,list2[1:])list1[0]<list2[0]otherwise

也就是说,两个链表头部值较小的一个节点与剩下元素的 m e r g e merge merge 操作结果合并。

算法

我们直接将以上递归过程建模,同时需要考虑边界情况。

如果 l 1 l1 l1 或者 l 2 l2 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l 1 l1 l1 l 2 l2 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。

class Solution:
    def mergeTwoLists(self, l1, l2):
        if l1 is None:
            return l2
        elif l2 is None:
            return l1
        elif l1.val < l2.val:
            l1.next = self.mergeTwoLists(l1.next, l2)
            return l1
        else:
            l2.next = self.mergeTwoLists(l1, l2.next)
            return l2

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/

2.2 迭代

思路

我们可以用迭代的方法来实现上述算法。当 l 1 l1 l1 l 2 l2 l2 都不是空链表时,判断 l 1 l1 l1 l 2 l2 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。

算法

首先,我们设定一个哨兵节点 p r e h e a d prehead prehead ,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 p r e v prev prev 指针,我们需要做的是调整它的 n e x t next next 指针。然后,我们重复以下过程,直到 l 1 l1 l1 或者 l 2 l2 l2 指向了 n u l nul null :如果 l 1 l1 l1 当前节点的值小于等于 l 2 l2 l2 ,我们就把 l1 当前的节点接在 p r e v prev prev 节点的后面同时将 l 1 l1 l1 指针往后移一位。否则,我们对 l 2 l2 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 p r e v prev prev 向后移一位。

在循环终止的时候, l 1 l1 l1 l 2 l2 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

class Solution:
    def mergeTwoLists(self, l1, l2):
        prehead = ListNode(-1)

        prev = prehead
        while l1 and l2:
            if l1.val <= l2.val:
                prev.next = l1
                l1 = l1.next
            else:
                prev.next = l2
                l2 = l2.next            
            prev = prev.next

        # 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        prev.next = l1 if l1 is not None else l2

        return prehead.next

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/

3. 复杂度分析

  • 递归
    时间复杂度 O ( n + m ) O(n + m) O(n+m),其中 n n n m m m 分别为两个链表的长度。因为每次调用递归都会去掉 l 1 l1 l1 或者 l 2 l2 l2 的头节点(直到至少有一个链表为空),函数 m e r g e T w o L i s t mergeTwoList mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O ( n + m ) O(n+m) O(n+m)
    空间复杂度 O ( n + m ) O(n + m) O(n+m),其中 n n n m m m 分别为两个链表的长度。递归调用 m e r g e T w o L i s t s mergeTwoLists mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 m e r g e T w o L i s t s mergeTwoLists mergeTwoLists 函数最多调用 n + m n+m n+m 次,因此空间复杂度为 O ( n + m ) O(n+m) O(n+m)

  • 迭代
    时间复杂度 O ( n + m ) O(n + m) O(n+m) ,其中 n n n m m m 分别为两个链表的长度。因为每次循环迭代中, l 1 l1 l1 l 2 l2 l2 只有一个元素会被放进合并链表中, 因此 w h i l e while while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O ( n + m ) O(n + m) O(n+m)
    空间复杂度 O ( 1 ) O(1) O(1)。我们只需要常数的空间存放若干变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值