老方法解新題


  這篇文章的特點就是方法都是老方法,但是使用在這個題目上就顯得更加的巧妙。刷的題多不是什麼值得誇讚的事情,希望大家都能夠把做過的題刷精,哪管他如何變化,看過去都是一道題,這纔是厲害的。

leetcode: 209

給定一個含有 n 個正整數的數組和一個正整數 s ,找出該數組中滿足其和 ≥ s的長度最小的連續子數組,並返回其長度。如果不存在符合條件的連續子數組,返回 0。

示例:

輸入: s = 7, nums = [2,3,1,2,4,3]
輸出: 2
解釋: 子數組 [4,3] 是該條件下的長度最小的連續子數組。

  這道題的暴力法大家很容易想到,那就是遍歷一個起始位置ii,遍歷一個終止位置jj,然後對起始位置到終止位置求和,看是否大於等於ss,然後從中找到跨度最小,也就是ji+1j-i+1最小的位置。

複雜度O(n2)O(n^2)的解法

  但是,大家應該保有一種思維就是,我不是每次去找一個iijj,顯然這樣可以找到O(n2)O(n^2)組,然後對每組獨立求和,這樣求和的複雜度是O(n)O(n)
  大家應該具有一種思維就是,我已經求出了一個iij1j_1,那麼再往上加一個數,可以直接在原來的和的基礎上加一個數,這樣每一-1次從iijj求和的複雜度就轉變成求si,j=si,j1+js_{i,j}=s_{i,j-1}+j,也就是O(1)O(1)。總的複雜度就是O(n2)O(n^2)
  到這裏,這個方法就很好理解了,這裏直接給出代碼。

python代碼

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        for i in range(1, len(nums)):
            if nums[i] >= s:return 1
            nums[i] += nums[i-1]
        nums += [0]
        for gap in range(1, len(nums)):
        	# 這裏直接使用的是子串的長度,而不是子串的起始位置
            for j in range(gap-1, len(nums)):
                if nums[j]-nums[j-gap] >= s:
                    return gap
        return 0

  值得一提的是,這裏使用了一個小技巧,就是python下標裏的-1,因爲想要直接在原數組上進行操作,直接在原數組最後面補了一個0,用這個作爲下標-1的位置來統一處理i=0的時候計算和,需要使用ss_{}的情況。
  如果到這裏就結束了,你會發現你的代碼被95%的人超過了,那麼本文也就沒有意義了。
  接下來的兩種方法,大家可以在看文章的時候,根據標題停頓,看看能否自己根據標題提示,找到更好的解法。

妙用二分查找

  在上一種方法中,事先已經對數組求好和了,我們得到的新數組nums[j]實際上就是從第0個位置開始,到當前位置,這個數組的和。顯然這個數組是遞增數組。
  我們可以在這個遞增數組中快速找到恰好比s大的第一個數,自然是使用二分查找。這樣實際上就是解決了,從第0個位置開始,大於等於s的最短子數組。
  同理,我們還需要解決從第一個位置開始的。。。但是方法都是一致的,二分查找就行了。
  這裏又得用到小技巧,我們第一輪求得了新數組nums[j]實際上就是從第0個位置開始,到當前位置,這個數組的和,如果我們要計算從第一個位置開始的最短子數組,自然是不用從第一個位置再求一次和的。
  我們從第0個位置求得和,不過是每一個數都比第零個·位置的和大了nums[0],也就是第1一個數之前所有的數之和。所以我們這個時候,可以繼續上新的nums,但是這次不是找s了,而是找s+nums[1-1],這裏的1可以換成任何一個i,都是同理。

python代碼

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        # 創造二分查找的條件,然後對每一個數開頭的列表都執行二分查找
        for i in range(1, len(nums)):
            if nums[i] >= s:return 1
            nums[i] += nums[i-1]
        nums += [0]

        res = len(nums)+1
        for i in range(len(nums)-2):
            start,end = i,len(nums)-2
            target = s+nums[i-1]
            while start <= end:
                mid = (start+end) >> 1
                if nums[mid]>=target and nums[mid-1] < target:
                    res = min(res, mid-i+1)
                    break
                elif nums[mid]>=target:
                    end = mid
                else:
                    start = mid+1
            else:
                break
        return 0 if res==len(nums)+1 else res

  代碼核心就是一個二分查找,找到第一個比s+nums[i-1]大的數。每一次查找的複雜度是O(lgn)O(lgn),因爲需要從n個位置開始查找,加上最開始求和的複雜度O(n)O(n),最終的複雜度就是O(nlgn)O(nlgn)

巧用雙指針

  這些方法似乎都是用數組從iijj的和。好像雙指針很適合來做這個事情,但是怎麼使用,還是需要做一番思考。保證雙指針儘量只往一個方向走,那麼就可以降低複雜度。
  這一種方法的關鍵就在於雙指針都是從小往大長。我們先固定住起始指針,然後增大結尾指針,直到我們找到了一個滿足的子串,然後我們需要記錄這個長度。
  這個時候,我們想要得到更好的解,自然不能繼續增大長度,而死減少長度,不用說,自然是增加起始指針。增加到什麼時候,增加到子串的和比s小了。這個時候自然不能繼續增大起始指針了,因爲越增大和越小,不能找到解。
  所以就這樣往復的增大結尾指針,和比s大了再增大起始指針,這樣往復。很像毛毛蟲向前蠕動。別叫雙指針了,叫毛毛蟲算法好了。

python代碼

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        # 雙指針法
        res = len(nums)+1
        sn = j = 0
        for i in range(len(nums)):
            sn += nums[i]

            while sn >= s:
                res = min(res, i-j+1)
                sn -= nums[j]
                j+=1
        return 0 if res==len(nums)+1 else res

  這樣到這裏算是大功告成了。可以看到其實方法都是已有的方法,但是需要把問題轉換成已知的問題。這個轉換過程纔是刷題最應該思考和掌握的。
  在這裏希望大家能夠在刷題中多學習思維,掌握一些基本套路是爲了構建更好的構架底層架構,轉換思維就像是在底層架構上構建的一套操作系統,所有的題目就是在操作系統上運行的用戶軟件。也希望大家能夠在日積月累中有所進步,穩定好底層架構,然後構建一套精妙的操作系統。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章