美文网首页
【每周算法】长度最小的子数组

【每周算法】长度最小的子数组

作者: 阙馨妍子 | 来源:发表于2020-06-29 23:43 被阅读0次

    写在前面:给自己定一个小目标,每周写一个程序员必须知道的算法题(大厂面试频率Top100),把这个养成和洗脸睡觉一样的周长习惯,每一个经典题目咬烂嚼碎,解题思路和源代码都会贴出来,有想学算法的可以跟上我的脚步,Follow me~

    题目描述

    给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0
    难度:mid
    示例

    输入:s = 7, nums = [2,3,1,2,4,3]
    输出:2
    解释:子数组 [4,3] 是该条件下的长度最小的连续子数组。
    

    进阶
    如果你已经完成了 O(n) 时间复杂度的解法, 请尝试 O(nlogn) 时间复杂度的解法

    题解

    双指针法

    时间复杂度O(n), 空间复杂度O(1)
    定义两个指针 leftright 分别表示子数组的开始位置和结束位置,初始状态下,leftright 都指向下标 0,子数组 cur_sum = 0
    每一轮迭代,如果子数组 cur_sum ≥ s,则更新子数组的最小长度,然后将 cur_sum 中减去 nums[left]v并将 left 右移,直到 cur_sum < s,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,right 右移。

    import datetime
    import bisect
    from typing import List
    
    
    class Solution:
        # 3. 双指针法
        def min_sub_array_len_3(self, s, nums: List[int]) -> int:
            if not nums:
                return 0
    
            len_nums = len(nums)
            left, cur_sum = 0, 0
            min_len = len_nums + 1
    
            for right in range(len_nums):
                cur_sum += nums[right]
                while cur_sum >= s:
                    min_len = min(min_len, right - left + 1)
                    cur_sum -= nums[left]
                    left += 1
    
            return min_len if min_len != len_nums + 1 else 0
    

    前缀和 + 二分查找法

    时间复杂度O(nlogn), 空间复杂度O(n)
    这里需要额外创建一个数组 sums 用于存储 nums 的前缀和,遍历有序数组 sums,需要找到 s + sums[i - 1] 的临界值 bound 的位置并更新最小长度。

        # 4. 前缀和 + 二分查找法
        def min_sub_array_len_4(self, s: int, nums: List[int]) -> int:
            if not nums:
                return 0
    
            len_nums = len(nums)
            min_len = len_nums + 1
            sums = [0]
    
            for i in range(len_nums):
                sums.append(sums[-1] + nums[i])
    
            for i in range(1, len_nums + 1):
                target = s + sums[i - 1]
                # 二分查找法:返回target应该插在sums中的下标位置
                bound = bisect.bisect_left(sums, target)
                if bound != len(sums):
                    min_len = min(min_len, bound - (i - 1))
    
            return min_len if min_len != len_nums + 1 else 0
    

    因为这道题数组中每个元素都为正,所以前缀和一定是递增的,否则这里就不能使用二分查找法了。
    虽然 Python 中有现成的库 bisect 为我们实现了二分查找,但有时面试官可能会想让我们自己实现这个函数,这里我也贴出二分查找的实现代码,供大家参考。

    def bisect_left(a, x, lo=0, hi=None):
        """Return the index where to insert item x in list a, assuming a is sorted.
    
        The return value i is such that all e in a[:i] have e < x, and all e in
        a[i:] have e >= x.  So if x already appears in the list, a.insert(x) will
        insert just before the leftmost x already there.
    
        Optional args lo (default 0) and hi (default len(a)) bound the
        slice of a to be searched.
        """
    
        if lo < 0:
            raise ValueError('lo must be non-negative')
        if hi is None:
            hi = len(a)
        while lo < hi:
            mid = (lo+hi)//2
            if a[mid] < x: lo = mid+1
            else: hi = mid
        return lo
    

    实际上本题我尝试用了4种方法,法1法2为暴力法,时间复杂度为O(n^2),解题思路比较好理解,但缺点也很明显,如果实在想不起上面两种方法,最后推荐大家使用这两种方法(千万不能让面试官觉得你连一种思路都没有)。

    暴力法

    时间复杂度O(n^2) 空间复杂度O(1)
    设最小长度为无穷大,遍历数组 nums,对于每个子数组的开始下标 i,需要找到 j >= i,使得 sum(nums[i:j]) >= s,并更新子数组的最小长度。

        # 1. 暴力法
        def min_sub_array_len_1(self, s: int, nums: List[int]) -> int:
            if not nums:
                return 0
    
            len_nums = len(nums)
            min_len = len_nums + 1
    
            for i in range(len_nums):
                cur_sum = 0
                for j in range(i, len_nums):
                    cur_sum += nums[j]
                    if cur_sum >= s:
                        min_len = min(min_len, j - i + 1)
                        break
    
            return min_len if min_len != len_nums + 1 else 0
    

    时间复杂度O(n^2) 空间复杂度O(1)
    设最小长度为 i+1,遍历数组 nums,对于每个子数组的开始下标 j,需要找到 sum(nums[j:j + i + 1]) >= s,并返回子数组的最小长度 i+1

        # 2.  暴力法
        def min_sub_array_len_2(self, s: int, nums: List[int]) -> int:
            if not nums:
                return 0
    
            len_nums = len(nums)
    
            for i in range(len_nums):
                for j in range(len_nums - i):
                    if sum(nums[j:j + i + 1]) >= s:
                        return i + 1
    

    总结

    综上4种解题方法,个人最推荐双指针法,虽然前缀和 + 二分查找法满足进阶要求,但可以看到,提高时间复杂度必然是要牺牲空间复杂度的,鱼和熊掌不可兼得,当应用到实际业务中,要根据时间更宝贵还是资源更重要来选择,所以没有哪一个更好,最好都能记住。如果实在记不住建议重点记忆双指针法,因为双指针法相对前缀和 + 二分查找法更好理解,而理解是记忆的基础,且双指针法已经是满足题目对时间复杂度的要求。

    解题万能方法:

    其实算法是有方法的,当有时间复杂度要求的时候,肯定不能用暴力法(即多层for循环遍历的情况),你往往需要考虑用一层循环来解决问题,而时间复杂度要求为O(logn)O(nlogn)时,就要考虑如何通过空间换时间,这时候空间复杂度上往往会从O(1)变为O(n),如本题的前缀和 + 二分查找法利用了前缀和sums数组,这就给你提供了一些解题思路,找到大体解决方向。


    除了上面的解题技巧,第二个我要分享给大家的就是学习金字塔原理,学习不是光靠努力追求阅读、试听的速度和数量,这会让人产生低层次的勤奋和成长的感觉,这只是在使蛮力。要思考,要实践,要总结和归纳,否则,你只是在机械地重复某件事,而不会有质的成长的。

    当年 LeetCode 只有 151 道题的时候,一共有十几万人上来做题,但全部做完的只有十几个,万分之一。所以,只要你能坚持,就可以超过这个世界上绝大多数人。当然,坚持也不是要苦苦地坚持,你要把坚持变成一种习惯,就像吃饭喝水一样,你感觉不到太多的成本付出。只有这样,你才能够真正坚持。 ---陈皓

    希望我的这些话可以让你有足够的动力坚持下去,欢迎大家关注我,和我一起坚持学习【每周算法】,直到将坚持养成习惯。另外有问题大家评论区讨论,积极沟通。如果本文对你有帮助,不要忘记点赞收藏,拒绝伸手党!

    相关文章

      网友评论

          本文标题:【每周算法】长度最小的子数组

          本文链接:https://www.haomeiwen.com/subject/qatnfktx.html