1. 程式人生 > 實用技巧 >Operating System:訊號量

Operating System:訊號量

LeetCode209. 長度最小的子陣列

方法一

暴力列舉所有可能的子陣列,也就是列舉子陣列的所有開始下標和結束下標,計運算元陣列的和,如果子陣列的和小於等於s,就更新最小長度。

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        if(nums.size() == 0) {                  //陣列為空的情況需要特判
            return 0;
        }
        int res = INT_MAX;                      //最終的返回結果,初始化為一個較大的數
        for(int i = 0; i < nums.size(); ++i) {  //列舉子陣列的起始下標
            int sum = 0;                        //子陣列的和
            for(int j = i; j < nums.size(); ++j) {     //列舉子陣列的結束下標
                sum += nums[j];
                if(sum >= s) {
                    res = min(res, j -i + 1);          //如果子陣列的和小於等於s,就更新res
                    break;                      //不要忘了加 break !!!
                }
            }
        }
        return res == INT_MAX ? 0 : res;
    }
};

方法二

同樣是列舉子陣列的起始下標,我們考慮優化子陣列結束下標的搜尋空間,如果能用二分找到(滿足子陣列的和大於等於s)子陣列結束下標的位置,相比於方法一就會更快,
二分查詢要求陣列是單調的,由於題目保證每一個元素都是正的,所以我們考慮用一個sums陣列記錄字首和,也就是說,sums[i]表示nums[0] ~ nums[i - 1]的元素總和,
這樣,問題就轉化為對於每一個起始下標i,通過二分查詢尋找一個大於等於i的最小下標bound,使得sums[bound] - sums[i - 1] >= s,sums[bound] - sums[i - 1]就是子陣列的和。找到了bound之後,我們就更新子陣列的最小長度(此時子陣列的長度是bound - (i - 1))。

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        if(nums.size() == 0) {
            return 0;
        }
        int size = nums.size();
        vector<int> sums(size + 1, 0);            //這裡sums的大小為size + 1,是因為我們要讓sums[0]為0,方便下一句的for迴圈對suns做初始化
        for(int i = 1; i <= size; ++i) {
            sums[i] = sums[i - 1] + nums[i - 1];
        }
        int res = INT_MAX;
        for(int i = 1; i <= size; ++i) {
            int target = s + sums[i - 1];         //target 為 s + sums[i - 1],也就是說如果某個數bound的字首和sums >= target,那麼以i為起始下標,到bound的子陣列的和就大於等於sum
            auto bound = lower_bound(sums.begin(), sums.end(), target);
            if(bound != sums.end()) {            //如果bound不越界,就更新子陣列的最小長度
                res = min(res, static_cast<int>((bound - sums.begin()) - (i - 1)));   
            }
        }
        return res == INT_MAX ? 0 : res;
    }
};

方法三

考慮用雙指標,兩個指標start和end分別表示子陣列的開始和結束下標,變數sum表示以start開頭,以end結尾的子陣列的和。start, end, sum的初始值都為0.
先將nums[end]加入sum中,如果sum < s,就繼續增加end(當然同時也把nums[end]加入sum)直到sum >= s,此時不增加end,而是更新子陣列的最小長度(此時長度為end - start + 1),然後不斷增加start(並更新子陣列的最小長度)直到sum < s,然後再繼續增加end,直到end超過了陣列最大下標。

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        if(nums.size() == 0) {
            return 0;
        }
        int start = 0, end = 0, sum = 0, res = INT_MAX;
        while(end < nums.size()) {
            sum += nums[end];
            while(sum >= s) {
                res = min(res, end - start + 1);
                sum -= nums[start];
                ++start;
            }
            ++end;
        }
        return res == INT_MAX ? 0 : res;
    }
};

複雜度分析

  1. 暴力方法中列舉起始下標需要O(n),內層迴圈列舉結束下標又需要O(n),因此時間複雜度是O(n);由於無需額外的輔助陣列,因此空間複雜度是O(1);
  2. 字首和 + 二分查詢同樣需要O(n)來列舉起始下標,但這裡對於子陣列的結束下標使用了二分查詢進行優化,查詢的複雜度是O(logn),因此總的時間複雜度是O(nlogn);由於需要一個額外陣列sums存放字首和,因此空間複雜度是O(n);
  3. 雙指標方法中,左右指標start和end都只會向右移動,並且對於每個起始下標,無需從頭開始列舉結束下標,因此時間複雜度是O(n);雙指標方法也無需額外的輔助陣列,空間複雜度為O(1).