1. 程式人生 > 其它 >陣列刷題筆記4:長度最小的子陣列

陣列刷題筆記4:長度最小的子陣列

長度最小的子陣列

209. 長度最小的子陣列

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

方法1:暴力解法

  • 思路:使用兩個for迴圈,不斷尋找符合條件的子序列。

  • 複雜度:

    • 時間複雜度:O(N^2),兩個for迴圈
    • 空間複雜度:O(1)

方法2:滑動視窗

  • 思路:
    • 視窗是滿足大小的長度最小的連續子陣列
    • 當視窗值大於s,起點指標移動
    • 視窗的結束位置是遍歷陣列的指標,視窗的起始位置為陣列的起始位置
  • 複雜度:
    • 時間複雜度:O(N),每個元素在滑動窗後進來操作一次,出去操作一次,每個元素都是被被操作兩次,所以時間複雜度是 2 × n 也就是O(n)
    • 空間複雜度:O(1)
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int result = n + 1;//設定為大於陣列長度
        int sum = 0; //滑動視窗內陣列的和
        int i = 0; //起點指標起始位置
        int subLength = 0; //滑動視窗的長度
        for (int j = 0; j < n; j++) {
            sum += nums[j];
            //如果和大於目標,開始移動起點位置
            while (sum >= target) {
                subLength = j - i + 1; //計算視窗長度
                result = result < subLength ? result : subLength; //對比修改當前的視窗長度
                sum -= nums[i++]; //移動起點,修改視窗和
            }
        }
        //判斷結果有沒有改變,如果沒有,不存在該改變長度的子陣列,返回0;有則返回結果
        return result == n + 1 ? 0 : result;
    }
}

904. 水果成籃

你正在探訪一家農場,農場從左到右種植了一排果樹。這些樹用一個整數陣列 fruits 表示,其中 fruits[i] 是第 i 棵樹上的水果 種類 。

你想要儘可能多地收集水果。然而,農場的主人設定了一些嚴格的規矩,你必須按照要求採摘水果:

  • 你只有 兩個 籃子,並且每個籃子只能裝 單一型別 的水果。每個籃子能夠裝的水果總量沒有限制。
  • 你可以選擇任意一棵樹開始採摘,你必須從 每棵 樹(包括開始採摘的樹)上 恰好摘一個水果 。採摘的水果應當符合籃子中的水果型別。每採摘一次,你將會向右移動到下一棵樹,並繼續採摘。
  • 一旦你走到某棵樹前,但水果不符合籃子的水果型別,那麼就必須停止採摘。

給你一個整數陣列 fruits ,返回你可以收集的水果的 最大 數目。

滑動視窗法(思路簡單,比較暴力)

  1. 設定n1, n2為陣列最開始兩個不重複的數
  2. 滑動到為不n1 n2的數,記錄上一個視窗長度,進行對比
  3. 重新賦值n1為視窗尾部指向的前一個數(上一次視窗中的其中一個數),n2為當前視窗尾部指向的數(新數)
  4. 將起始指標從結束指標向前遍歷為第一個n1的位置
  5. 繼續滑動視窗尾部,重複操作
  6. 結尾沒改變時沒進入迴圈,最後計算一次視窗長度
class Solution {
    public int totalFruit(int[] fruits) {
        //使用滑動視窗找最長不重複的兩個數字
        int n = fruits.length;
        if (n <= 2) return n; //長度小於2時,直接返回結果
        
        int result = 2; //結果
        int i = 0; //起始指標
        int subLength = 0; //滑動視窗的長度
        int n1 = fruits[0]; //設定兩個籃子裡的數
        //給n2賦值為與第一個數不同的第一個數
        int n2 = 0;
        for (int j = 0; j < n; j++) {
            if (fruits[j] != n1) {
                n2 = fruits[j];
                break;
            }
        }

        //開始滑動視窗
        //滑動到為不n1 n2的數,記錄上一個視窗長度,進行對比
        //重新賦值n1為視窗尾部指向的前一個數(上一次視窗中的其中一個數),n2為當前視窗尾部指向的數(新數)
        //將起始指標從結束指標向前遍歷為第一個n1的位置
        //繼續滑動視窗尾部,重複操作
        for (int j = 0; j < n; j++) {
            while ((fruits[j] != n1) && (fruits[j] != n2)) {
                subLength = j - i; //此時j指向下一個不連續的元素,因此不需要+1
                result = result > subLength ? result : subLength; //改變長度
                n1 = fruits[j - 1]; //重新賦值n1為不同數的前一個數
                n2 = fruits[j]; //重新賦值n2為不同的數
                
                //將i向前遍歷移動到第一個n1值的位置
                i = j - 1; 
                while (fruits[i - 1] == fruits[i]) {
                    i--;
                }
            }
        }
        //結尾沒改變時沒進入迴圈,最後計算一次視窗長度
        subLength = n - i; //陣列長度為末尾j指向的下一個元素,不需要再+1
        result = result > subLength ? result : subLength;
        
        return result;
    }
}

滑動視窗(計算水果種類,出現頻次)

  • 思路:滑動視窗系列中的計數問題,freq陣列記錄現在視窗中兩種水果的數目,count記錄現在視窗中有多少種不同的水果,如果count<=2,那麼right指標可以一直向右移動,直到count>2,當count>2時,說明視窗中的水果種類超過2,為了減少水果種類,left指標就要向右移動,移動的同時還要更新freq陣列,最後取最大值即可。
class Solution {
    public int totalFruit(int[] fruits) {
        int n = fruits.length;
        if (n <= 2) return n;
        int total = 2;
        int left = 0;
        int count = 0; // 計算籃中種類數
        int[] fruitFrequence = new int[n]; // 計算籃中每種水果出現的次數。 因為提示中說明了水果的種類數是有限的 0 <= fruits[i] < fruits.length
        for (int right = 0; right < n; right++) {
            fruitFrequence[fruits[right]] += 1; // 入籃
            if (fruitFrequence[fruits[right]] == 1) count+=1; //等於1說明第一次入籃,count需要加1
            while (count > 2) { // 籃中超過兩種水果
                fruitFrequence[fruits[left]] -= 1;  // 因為下邊還要使用left下標,所以先不要移動
                if (fruitFrequence[fruits[left]] == 0) count-=1; // 等於0說明籃中已經沒有fruits[left]水果,count減1
                left += 1; // 移動left
            }
            total = Math.max(total, right - left + 1); // 取視窗最大值
        }
        return total;
    }
}

76. 最小覆蓋子串(困難)

給你一個字串 s 、一個字串 t 。返回 s 中涵蓋 t 所有字元的最小子串。如果 s 中不存在涵蓋 t 所有字元的子串,則返回空字串 "" 。

注意:

對於 t 中重複字元,我們尋找的子字串中該字元數量必須不少於 t 中該字元數量。
如果 s 中存在這樣的子串,我們保證它是唯一的答案。

滑動視窗法(陣列)

  1. 右指標移動,直到包含t內的所有元素
  2. 計算長度和起點
  3. 左指標移動,如果有更小的區間更新長度和起點
  4. 直到區間不包含t,移動右指標,重複以上步驟

理解:

在遍歷字串的過程中,非目標字元的數量會被減到-1,因此增加count的計數。

當遍歷到目標字元時,目標字元原本為0,計數器count+1,在執行後字元對應的值大於0,在進入下一次迴圈時,值減為一,並減少計數器的值,從而進入視窗的迴圈。

class Solution {
    public String minWindow(String s, String t) {
        //右指標移動,直到包含t內的所有元素
        //計算長度和起點
        //左指標移動,如果有更小的區間更新長度和起點
        //直到區間不包含t,移動右指標,重複以上步驟

        //如果s的長度小於t的長度,那s必然無法包含t的所有元素
        //如果s與t相等,那結果為s或t
        if (s.length() < t.length()) {
            return "";
        } else if (s.equals(t)) {
            return s;
        }

        int left = 0; //左指標
        int right = 0; //右指標
        int count = t.length(); //記錄未包含元素個數
        int windowLength = s.length() + 1; //視窗長度
        int start = 0; //視窗起點
        
        //記錄t中各個元素個數的陣列(由英文字母組成,因此由字元所對應的數字的下標進行計數
        int[] map = new int[128];
        for (char c : t.toCharArray()) map[c]++;

        while (right < s.length()) {
            if (map[s.charAt(right++)]-- > 0) {
                count--;
            }

            while (count == 0) {
                if (right - left < windowLength) {
                    start = left;
                    windowLength = right - left;
                }

                if (map[s.charAt(left++)]++ == 0) {
                    count++;
                }
            }
        }
        if (windowLength != s.length() + 1) {
            return s.substring(start, start + windowLength);
        }
        return "";
    }
}