今天咱們來講一下刷題時常常用到的前綴和思想,前綴和思想和滑動窗口會常常用在求子數組和子串問題上,當咱們遇到此類問題時,則應該須要想到此類解題方式,該文章深刻淺出描述前綴和思想,讀完這個文章就會有屬於本身的解題框架,遇到此類問題時就可以輕鬆應對。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
題目描述框架
給定一個整數類型的數組 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;
}
}
複製代碼
題目描述
給定一個整數數組和一個整數 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 的個數
好啦咱們來看一下代碼吧
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. 和相同的二元子數組
題目描述
給你一個整數數組 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 改爲了判斷奇偶的語句,見下圖。
咱們來解析一下哈希表,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;
}
}
複製代碼
題目描述
給定一個整數數組 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的子數組 **中,咱們須要求出知足條件的區間,見下圖
咱們須要找到知足,和爲 K 的區間。咱們此時 presum 是已知的,k 也是已知的,咱們只須要找到 presum - k區間的個數,就能獲得 k 的區間個數。可是咱們在當前題目中應該怎麼作呢?見下圖。
咱們在以前的例子中說到,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 的區間個數。見下圖
題目代碼
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;
}
}
複製代碼
題目描述
給定一個包含 非負數 的數組和一個目標 整數 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,那麼咱們應該怎麼判斷子數組的長度呢?咱們能夠根據索引來進行判斷,見下圖。
此時咱們 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,若是咱們保存的是較大的那個索引,則會出現下列狀況,見下圖。
此時,仍會顯示不知足子區間長度至少爲 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;
}
}
複製代碼