得了,我這下把前綴和給扒的乾乾淨淨了。

今天咱們來講一下刷題時常常用到的前綴和思想,前綴和思想和滑動窗口會常常用在求子數組和子串問題上,當咱們遇到此類問題時,則應該須要想到此類解題方式,該文章深刻淺出描述前綴和思想,讀完這個文章就會有屬於本身的解題框架,遇到此類問題時就可以輕鬆應對。java

下面咱們先來了解一下什麼是前綴和。git

前綴和其實咱們很早以前就瞭解過的,咱們求數列的和時,Sn = a1+a2+a3+...an; 此時Sn就是數列的前 n 項和。例 S5 = a1 + a2 + a3 + a4 + a5; S2 = a1 + a2。因此咱們徹底能夠經過 S5-S2 獲得 a3+a4+a5 的值,這個過程就和咱們作題用到的前綴和思想相似。咱們的前綴和數組裏保存的就是前 n 項的和。見下圖github

咱們經過前綴和數組保存前 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];
 }
複製代碼

好啦,咱們開始實戰吧。markdown

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

題目描述框架

給定一個整數類型的數組 nums,請編寫一個可以返回數組 「中心索引」 的方法。函數

咱們是這樣定義數組 中心索引 的:數組中心索引的左側全部元素相加的和等於右側全部元素相加的和。oop

若是數組不存在中心索引,那麼咱們應該返回 -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的子數組](cdn.jsdelivr.net/gh/tan45du/… 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 連續的子數組和](cdn.jsdelivr.net/gh/tan45du/… 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;
    }
}
複製代碼
相關文章
相關標籤/搜索