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

写在前面:给自己定一个小目标,每周写一个程序员必须知道的算法题(大厂面试频率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 道题的时候,一共有十几万人上来做题,但全部做完的只有十几个,万分之一。所以,只要你能坚持,就可以超过这个世界上绝大多数人。当然,坚持也不是要苦苦地坚持,你要把坚持变成一种习惯,就像吃饭喝水一样,你感觉不到太多的成本付出。只有这样,你才能够真正坚持。 ---陈皓

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350