得了,一文把前綴和給扒的干干凈凈了。


今天我們來說一下刷題時經常用到的前綴和思想,前綴和思想和滑動窗口會經常用在求子數組和子串問題上,當我們遇到此類問題時,則應該需要想到此類解題方式,該文章深入淺出描述前綴和思想,讀完這個文章就會有屬於自己的解題框架,遇到此類問題時就能夠輕松應對。

下面我們先來了解一下什么是前綴和。

前綴和其實我們很早之前就了解過的,我們求數列的和時,Sn = a1+a2+a3+...an; 此時Sn就是數列的前 n 項和。例 S5 = a1 + a2 + a3 + a4 + a5; S2 = a1 + a2。所以我們完全可以通過 S5-S2 得到 a3+a4+a5 的值,這個過程就和我們做題用到的前綴和思想類似。我們的前綴和數組里保存的就是前 n 項的和。見下圖

我們通過前綴和數組保存前 n 位的和,presum[1]保存的就是 nums 數組中前 1 位的和,也就是 presum[1] = nums[0], presum[2] = nums[0] + nums[1] = presum[1] + nums[1]. 依次類推,所以我們通過前綴和數組可以輕松得到每個區間的和。

例如我們需要獲取 nums[2] 到 nums[4] 這個區間的和,我們則完全根據 presum 數組得到,是不是有點和我們之前說的字符串匹配算法中 BM,KMP 中的 next 數組和 suffix 數組作用類似。那么我們怎么根據 presum 數組獲取 nums[2] 到 nums[4] 區間的和呢?見下圖

前綴和

好啦,我們已經了解了前綴和的解題思想了,我們可以通過下面這段代碼得到我們的前綴和數組,非常簡單

 for (int i = 0; i < nums.length; i++) {
      presum[i+1] = nums[i] + presum[i];
 }

好啦,我們開始實戰吧。

leetcode 724. 尋找數組的中心索引

題目描述

給定一個整數類型的數組 nums,請編寫一個能夠返回數組 “中心索引” 的方法。

我們是這樣定義數組 中心索引 的:數組中心索引的左側所有元素相加的和等於右側所有元素相加的和。

如果數組不存在中心索引,那么我們應該返回 -1。如果數組有多個中心索引,那么我們應該返回最靠近左邊的那一個。

示例 1:

輸入:
nums = [1, 7, 3, 6, 5, 6]
輸出:3

解釋:
索引 3 (nums[3] = 6) 的左側數之和 (1 + 7 + 3 = 11),與右側數之和 (5 + 6 = 11) 相等。
同時, 3 也是第一個符合要求的中心索引。

示例 2:

輸入:
nums = [1, 2, 3]
輸出:-1

解釋:
數組中不存在滿足此條件的中心索引。

理解了我們前綴和的概念(不知道好像也可以做,這個題太簡單了哈哈)。我們可以一下就能把這個題目做出來,先遍歷一遍求出數組的和,然后第二次遍歷時,直接進行對比左半部分和右半部分是否相同,如果相同則返回 true,不同則繼續遍歷。

class Solution {
    public int pivotIndex(int[] nums) {
        int presum = 0;
        //數組的和
        for (int x : nums) {
           presum += x;
        }      
        int leftsum = 0;
        for (int i = 0; i < nums.length; ++i) {
            //發現相同情況
            if (leftsum == presum - nums[i] - leftsum) {
                return i;
            }
            leftsum += nums[i];          
        }
        return -1;
    }
}

leetcode 560. 和為K的子數組

題目描述

給定一個整數數組和一個整數 k,你需要找到該數組中和為 k 的連續的子數組的個數。

示例 1 :

輸入:nums = [1,1,1], k = 2
輸出: 2 , [1,1] 與 [1,1] 為兩種不同的情況。

暴力法

解析

我們先來用暴力法解決這個題目,很簡單,一下就能 AC。

這個題目的題意很容易理解,就是讓我們返回和為 k 的子數組的個數,所以我們直接利用雙重循環解決該題,這個是很容易想到的。我們直接看代碼吧。

class Solution {
    public int subarraySum(int[] nums, int k) {
         int len = nums.length;
         int sum = 0;
         int count = 0;
         //雙重循環
         for (int i = 0; i < len; ++i) {
             for (int j = i; j < len; ++j) {
                 sum += nums[j];
                 //發現符合條件的區間
                 if (sum == k) {
                     count++;
                 }
             }
             //記得歸零,重新遍歷
             sum = 0;
         }
         return count;
    }
}

好啦,既然我們已經知道如何求前綴和數組了,那我們來看一下如何用前綴和思想來解決這個問題。

class Solution {
    public int subarraySum(int[] nums, int k) {
        //前綴和數組
        int[] presum = new int[nums.length+1];
        for (int i = 0; i < nums.length; i++) {
            //這里需要注意,我們的前綴和是presum[1]開始填充的
            presum[i+1] = nums[i] + presum[i];
        }
        //統計個數
        int count = 0;
        for (int i = 0; i < nums.length; ++i) {
            for (int j = i; j < nums.length; ++j) {
                //注意偏移,因為我們的nums[2]到nums[4]等於presum[5]-presum[2]
                //所以這樣就可以得到nums[i,j]區間內的和
                if (presum[j+1] - presum[i] == k) {
                    count++;
                }
            }
        }
        return count;
    }
}

我們分析上面的代碼,發現該代碼雖然用到了前綴和數組,但是對比暴力法並沒有提升性能,時間復雜度仍為O(n^2),空間復雜度成了 O(n)。那我們有沒有其他方法解決呢?

前綴和 + HashMap

了解這個方法前,我們先來看一下下面這段代碼,保證你很熟悉

class Solution {
    public int[] twoSum(int[] nums, int target) {

        HashMap<Integer,Integer> map  = new HashMap<>();
        //一次遍歷
        for (int i = 0; i < nums.length; ++i) {
            //存在時,我們用數組得值為 key,索引為 value
            if (map.containsKey(target - nums[i])){              
               return new int[]{i,map.get(target-nums[i])};
            }
            //存入值
            map.put(nums[i],i);
        }
        //返回
        return new int[]{};
    }
}

上面的這段代碼是不是賊熟悉,沒錯就是那個快被我們做爛的兩數之和。這一段代碼就是用到了我們的前綴和+ HashMap 思想,那么我們如何通過這個方法來解決這個題目呢?

在上面的代碼中,我們將數組的值和索引存入 map 中,當我們遍歷到某一值 x 時,判斷 map 中是否含有 target - x,即可。

其實我們現在這個題目和兩數之和原理是一致的,只不過我們是將所有的前綴和該前綴和出現的次數存到了 map 里。下面我們來看一下代碼的執行過程。

![leetcode 560 和為k的子數組](https://cdn.jsdelivr.net/gh/tan45du/github.io.phonto2@master/myphoto/leetcode 560 和為k的子數組.22vke3otf8sg.gif)

我們來拆解一下動圖,可能有的同學會思考為什么我們只要查看是否含有 presum - k ,並獲取到presum - k 出現的次數就行呢?見下圖,所以我們完全可以通過 presum - k的個數獲得 k 的個數

微信截圖_20210115194113

好啦我們來看一下代碼吧

class Solution {
    public int subarraySum(int[] nums, int k) {
        if (nums.length == 0) {
            return 0;
        }
        HashMap<Integer,Integer> map = new HashMap<>();
        //細節,這里需要預存前綴和為 0 的情況,會漏掉前幾位就滿足的情況
        //例如輸入[1,1,0],k = 2 如果沒有這行代碼,則會返回0,漏掉了1+1=2,和1+1+0=2的情況
        //輸入:[3,1,1,0] k = 2時則不會漏掉
        //因為presum[3] - presum[0]表示前面 3 位的和,所以需要map.put(0,1),墊下底
        map.put(0, 1);
        int count = 0;
        int presum = 0;
        for (int x : nums) {
            presum += x;
            //當前前綴和已知,判斷是否含有 presum - k的前綴和,那么我們就知道某一區間的和為 k 了。
            if (map.containsKey(presum - k)) {
                count += map.get(presum - k);//獲取次數
            }
            //更新
            map.put(presum,map.getOrDefault(presum,0) + 1);
        }
        return count;
    }
}

做完這個題目,各位也可以去完成一下這個題目,兩個題目幾乎完全相同 leetcode 930. 和相同的二元子數組

leetcode1248. 統計「優美子數組」

題目描述

給你一個整數數組 nums 和一個整數 k。

如果某個 連續 子數組中恰好有 k 個奇數數字,我們就認為這個子數組是「優美子數組」。

請返回這個數組中「優美子數組」的數目。

示例 1:

輸入:nums = [1,1,2,1,1], k = 3
輸出:2
解釋:包含 3 個奇數的子數組是 [1,1,2,1] 和 [1,2,1,1] 。

示例 2:

輸入:nums = [2,4,6], k = 1
輸出:0
解釋:數列中不包含任何奇數,所以不存在優美子數組。

示例 3:

輸入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
輸出:16

如果上面那個題目我們完成了,這個題目做起來,分分鍾的事,不信你去寫一哈,百分百就整出來了,我們繼續按上面的思想來解決。

HashMap

解析

上個題目我們是求和為 K 的子數組,這個題目是讓我們求 恰好有 k 個奇數數字的連續子數組,這兩個題幾乎是一樣的,上個題中我們將前綴區間的和保存到哈希表中,這個題目我們只需將前綴區間的奇數個數保存到區間內即可,只不過將 sum += x 改成了判斷奇偶的語句,見下圖。

微信截圖_20210114222339

我們來解析一下哈希表,key 代表的是含有 1 個奇數的前綴區間,value 代表這種子區間的個數,含有兩個,也就是nums[0],nums[0,1].后面含義相同,那我們下面直接看代碼吧,一下就能讀懂。

class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        
        if (nums.length == 0) {
            return 0;
        }
        HashMap<Integer,Integer> map = new HashMap<>();
        //統計奇數個數,相當於我們的 presum
        int oddnum = 0;
        int count = 0;
        map.put(0,1);
        for (int x : nums) {
            // 統計奇數個數
            oddnum += x & 1;
            // 發現存在,則 count增加
            if (map.containsKey(oddnum - k)) {
             count += map.get(oddnum - k);
            }
            //存入
            map.put(oddnum,map.getOrDefault(oddnum,0)+1);
        }
        return count;
    }
}

但是也有一點不同,就是我們是統計奇數的個數,數組中的奇數個數肯定不會超過原數組的長度,所以這個題目中我們可以用數組來模擬 HashMap ,用數組的索引來模擬 HashMap 的 key,用值來模擬哈希表的 value。下面我們直接看代碼吧。

class Solution {
    public int numberOfSubarrays(int[] nums, int k) {      
        int len = nums.length;
        int[] map = new int[len + 1];
        map[0] = 1;
        int oddnum = 0;
        int count = 0;
        for (int i = 0; i < len; ++i) {
            //如果是奇數則加一,偶數加0,相當於沒加
            oddnum += nums[i] & 1;
            if (oddnum - k >= 0) {
                count += map[oddnum-k];
            }
            map[oddnum]++;
        }
        return count;
    }
}

leetcode 974 和可被 K 整除的子數組

題目描述

給定一個整數數組 A,返回其中元素之和可被 K 整除的(連續、非空)子數組的數目。

示例:

輸入:A = [4,5,0,-2,-3,1], K = 5
輸出:7

解釋:

有 7 個子數組滿足其元素之和可被 K = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]

前綴和+HashMap

解析

我們在該文的第一題 **和為K的子數組 **中,我們需要求出滿足條件的區間,見下圖

微信截圖_20210115194113

我們需要找到滿足,和為 K 的區間。我們此時 presum 是已知的,k 也是已知的,我們只需要找到 presum - k區間的個數,就能得到 k 的區間個數。但是我們在當前題目中應該怎么做呢?見下圖。

微信截圖_20210115150520

我們在之前的例子中說到,presum[j+1] - presum[i] 可以得到 nums[i] + nums[i+1]+.... nums[j],也就是[i,j]區間的和。

那么我們想要判斷區間 [i,j] 的和是否能整除 K,也就是上圖中紫色那一段是否能整除 K,那么我們只需判斷

(presum[j+1] - presum[i] ) % k 是否等於 0 即可,

我們假設 (presum[j+1] - presum[i] ) % k == 0;則

presum[j+1] % k - presum[i] % k == 0;

presum[j +1] % k = presum[i] % k ;

我們 presum[j +1] % k 的值 key 是已知的,則是當前的 presum 和 k 的關系,我們只需要知道之前的前綴區間里含有相同余數 (key)的個數。則能夠知道當前能夠整除 K 的區間個數。見下圖

微信截圖_20210115152113

題目代碼

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        HashMap<Integer,Integer> map = new HashMap<>();
        map.put(0,1);
        int presum = 0;
        int count = 0;
        for (int x : A) {
             presum += x;
             //當前 presum 與 K的關系,余數是幾,當被除數為負數時取模結果為負數,需要糾正
             int key = (presum % K + K) % K;
             //查詢哈希表獲取之前key也就是余數的次數
             if (map.containsKey(key)) {
                 count += map.get(key);
             }
             //存入哈希表當前key,也就是余數
             map.put(key,map.getOrDefault(key,0)+1);
        }
        return count;
    }
}

我們看到上面代碼中有一段代碼是這樣的

int key = (presum % K + K) % K;

這是為什么呢?不能直接用 presum % k 嗎?

這是因為當我們 presum 為負數時,需要對其糾正。糾正前(-1) %2 = (-1),糾正之后 ( (-1) % 2 + 2) % 2=1 保存在哈希表中的則為 1.則不會漏掉部分情況,例如輸入為 [-1,2,9],K = 2如果不對其糾正則會漏掉區間 [2] 此時 2 % 2 = 0,符合條件,但是不會被計數。

那么這個題目我們可不可以用數組,代替 map 呢?當然也是可以的,因為此時我們的哈希表存的是余數,余數最大也只不過是 K-1所以我們可以用固定長度 K 的數組來模擬哈希表。

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        int[] map = new int[K];
        map[0] = 1;
        int len = A.length;
        int presum = 0;
        int count = 0;
        for (int i = 0; i < len; ++i) {
            presum += A[i];
            //求key
            int key = (presum % K + K) % K;
            //count添加次數,並將當前的map[key]++;
            count += map[key]++;         
        }
        return count;
    }
}

leetcode 523 連續的子數組和

題目描述

給定一個包含 非負數 的數組和一個目標 整數 k,編寫一個函數來判斷該數組是否含有連續的子數組,其大小至少為 2,且總和為 k 的倍數,即總和為 n*k,其中 n 也是一個整數。

示例 1:

輸入:[23,2,4,6,7], k = 6
輸出:True

解釋:[2,4] 是一個大小為 2 的子數組,並且和為 6。

示例 2:

輸入:[23,2,6,4,7], k = 6
輸出:True

解釋:[23,2,6,4,7]是大小為 5 的子數組,並且和為 42。

前綴和 + HashMap

這個題目算是對剛才那個題目的升級,前半部分是一樣的,都是為了讓你找到能被 K 整除的子數組,但是這里加了一個限制,那就是子數組的大小至少為 2,那么我們應該怎么判斷子數組的長度呢?我們可以根據索引來進行判斷,見下圖。

微信截圖_20210115174825

此時我們 K = 6, presum % 6 = 4 也找到了相同余數的前綴子數組 [0,1] 但是我們此時指針指向為 2,我們的前綴子區間 [0,1]的下界為1,所以 2 - 1 = 1,但我們的中間區間的長度小於2,所以不能返回 true,需要繼續遍歷,那我們有兩個區間[0,1],[0,2]都滿足 presum % 6 = 4,那我們哈希表中保存的下標應該是 1 還是 2 呢?我們保存的是1,如果我們保存的是較大的那個索引,則會出現下列情況,見下圖。

微信截圖_20210115175122

此時,仍會顯示不滿足子區間長度至少為 2 的情況,仍會繼續遍歷,但是我們此時的 [2,3]區間已經滿足該情況,返回 true,所以我們往哈希表存值時,只存一次,即最小的索引即可。下面我們看一下該題的兩個細節

細節1:我們的 k 如果為 0 時怎么辦,因為 0 不可以做除數。所以當我們 k 為 0 時可以直接存到數組里,例如輸入為 [0,0] , K = 0 的情況

細節2:另外一個就是之前我們都是統計個數,value 里保存的是次數,但是此時我們加了一個條件就是長度至少為 2,保存的是索引,所以我們不能繼續 map.put(0,1),應該賦初值為 map.put(0,-1)。這樣才不會漏掉一些情況,例如我們的數組為[2,3,4],k = 1,當我們 map.put(0,-1) 時,當我們遍歷到 nums[1] 即 3 時,則可以返回 true,因為 1-(-1)= 2,5 % 1=0 , 同時滿足。

視頻解析

![leetcode 523 連續的子數組和](https://cdn.jsdelivr.net/gh/tan45du/github.io.phonto2@master/myphoto/leetcode 523 連續的子數組和.1dgqjn0e8we8.gif)

題目代碼

class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        HashMap<Integer,Integer> map = new HashMap<>();
        //細節2
        map.put(0,-1);
        int presum = 0;
        for (int i = 0; i < nums.length; ++i) {
            presum += nums[i];
            //細節1,防止 k 為 0 的情況
            int key = k == 0 ? presum : presum % k;
            if (map.containsKey(key)) {
                if (i - map.get(key) >= 2) {
                     return true;
                }
                //因為我們需要保存最小索引,當已經存在時則不用再次存入,不然會更新索引值
                continue;           
            } 
            map.put(key,i);                  
        }
        return false;
    }
}

好啦,前綴和能寫的都在這了,看完妥妥的搞定這些題目,很簡單,而且還會有自己的解題思維,遇到類似題目一點不慌。

另外我們新建了一個刷題打卡小隊,每天用小程序打卡互相監督,群內還有幾位大佬進行指導講解,個人認為還不錯。需要的可以看我簡介
另外還聯合幾位老哥整理了一份經典必刷題,題目大綱,需要的可以看我的個人簡介,在小屋內點擊刷題大綱查看。
qrcode_for_gh_1f36d2ef6df9_258


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM