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)。我们只需要常数的空间存放若干变量。

好的,以下是LeetCode 635用C语言实现的代码,希望能够帮到您。 目描述: 设计一个简易日志系统,支持以下操作: 1. 新增一条日志到日志系统中。 2. 统计日志系统中某个给定时间段内的日志数量。 3. 统计日志系统中所有日志的数量。 实现: ```c typedef struct { char* id; int timestamp; } Log; typedef struct { Log* logs; int size; int capacity; } LogSystem; LogSystem* logSystemCreate() { LogSystem* obj = (LogSystem*)malloc(sizeof(LogSystem)); obj->logs = (Log*)malloc(sizeof(Log) * 1001); obj->size = 0; obj->capacity = 1001; return obj; } void logSystemAdd(LogSystem* obj, int id, char* timestamp) { obj->logs[obj->size].id = (char*)malloc(sizeof(char) * 15); sprintf(obj->logs[obj->size].id, "%d", id); char year[5], month[3], day[3], hour[3], minute[3], second[3]; strncpy(year, timestamp, 4); year[4] = '\0'; strncpy(month, timestamp + 5, 2); month[2] = '\0'; strncpy(day, timestamp + 8, 2); day[2] = '\0'; strncpy(hour, timestamp + 11, 2); hour[2] = '\0'; strncpy(minute, timestamp + 14, 2); minute[2] = '\0'; strncpy(second, timestamp + 17, 2); second[2] = '\0'; obj->logs[obj->size].timestamp = atoi(year) * 100000000 + atoi(month) * 1000000 + atoi(day) * 10000 + atoi(hour) * 100 + atoi(minute); obj->size++; } int* logSystemRetrieve(LogSystem* obj, char* s, char* e, char* gra, int* returnSize) { int start, end, len; int* res = (int*)malloc(sizeof(int) * obj->size); *returnSize = 0; if (strcmp(gra, "Year") == 0) { start = atoi(strncpy(s, s, 4)) * 1000000; end = atoi(strncpy(e, e, 4)) * 1000000; len = 4; } else if (strcmp(gra, "Month") == 0) { start = atoi(strncpy(s, s, 7)) * 10000; end = atoi(strncpy(e, e, 7)) * 10000; len = 7; } else if (strcmp(gra, "Day") == 0) { start = atoi(strncpy(s, s, 10)) * 100; end = atoi(strncpy(e, e, 10)) * 100; len = 10; } else if (strcmp(gra, "Hour") == 0) { start = atoi(strncpy(s, s, 13)); end = atoi(strncpy(e, e, 13)); len = 13; } else if (strcmp(gra, "Minute") == 0) { start = atoi(strncpy(s, s, 16)); end = atoi(strncpy(e, e, 16)); len = 16; } else { start = atoi(strncpy(s, s, 19)); end = atoi(strncpy(e, e, 19)); len = 19; } for (int i = 0; i < obj->size; i++) { int time = obj->logs[i].timestamp; if (time >= start && time < end) { res[(*returnSize)++] = atoi(obj->logs[i].id); } } int* result = (int*)malloc(sizeof(int) * (*returnSize)); for (int i = 0; i < *returnSize; i++) { result[i] = res[i]; } free(res); return result; } void logSystemFree(LogSystem* obj) { for (int i = 0; i < obj->size; i++) { free(obj->logs[i].id); } free(obj->logs); free(obj); } ``` 这段代码实现了一个简单的日志系统,包含了新增日志、统计日志数量等操作。在这个实现中,我们使用了结构体Log和LogSystem来表示日志和日志系统。其中,Log包含了日志的id和时间戳,LogSystem包含了日志数组、数组大小和数组容量。 在新增日志的操作中,我们将日志的id和时间戳存储到日志对象中,同时将日志对象存储到日志数组中。 在统计日志数量的操作中,我们首先根据粒度参数gra将起始时间s和结束时间e转换成整数形式,然后遍历日志数组,统计符合要求的日志数量。 最后,在释放日志系统对象的操作中,我们需要将每个日志对象中的id字符串释放掉,并释放日志数组和日志系统对象本身所占用的内存。 希望这段代码能够帮到您,如果您有任何问或疑问,请随时向我提出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值