Leetcode刷题总结
前缀和:
前缀和常用与求解子数组的问题,最常见的就是子数组求和问题。
定义 p r e [ i ] pre[i] pre[i]为数组从0~i位置的和, s u m ( i , j ) sum(i,j) sum(i,j)表示从i位置到j位置的子数组的和,则有 s u m ( i , j ) = p r e ( j ) − p r e ( i ) sum(i,j) = pre(j) - pre(i) sum(i,j)=pre(j)−pre(i)
根据上述的推论可以很轻易的求解出任意一段子数组的和
1. Leetcode560 和为k的子数组
题目描述:给定一个整数数组和一个整数 **k,**你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1:
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
思路:
根据题目的描述,我们可以想到使用前缀和来解决这一题。即只要两个前缀和之差满足 p r e [ j ] − p r e [ i ] = k pre[j]-pre[i]=k pre[j]−pre[i]=k即视为找到了一种情况。
算法步骤:
- 顺序遍历数组,计算每个位置的前缀和 p r e [ i ] pre[i] pre[i]
- 寻找前缀和之差为k的组合,计数
进阶优化:
在算法步骤2中,如果是使用蛮力的方法,需要用双循环来寻找满足条件的组合,时间复杂度为Θ(n^2)
不妨把 p r e [ i ] pre[i] pre[i]与 p r e [ j ] pre[j] pre[j]的组合记录为一种状态,那么我们很容易就可以分析出来上述方法低效的原因,因为没有记住这种状态。下一次遇到相同的 p r e [ i ] pre[i] pre[i]或 p r e [ j ] pre[j] pre[j],仍然需要做一次判断。
因此优化的方法就是使用哈希表记录下当前已经判断过的前缀和,(参考Leetcode3 两数之和)利用哈希表的快速查找的特点,可以使时间复杂度降低为Θ(n)。当遍历到前缀和 p r e [ i ] pre[i] pre[i]时,哈希表记录下 p r e [ i ] pre[i] pre[i],如果后续遍历有 p r e [ j ] pre[j] pre[j]满足 p r e [ j ] − k = = p r e [ i ] pre[j]-k == pre[i] pre[j]−k==pre[i],其差值必然可以在哈希表中找到。
代码
int subarraySum(vector<int> &nums, int k)
{
int len = nums.size();
if (len == 0) return 0;
unordered_map<int, int> m;
m[0] = 1; //没有元素时,前缀和为0(对应特例为一个值为k的元素)
int sum = 0;
int count = 0;
for (int i = 0; i < len; i++)
{
sum += nums[i];
if (m.find(sum - k) != m.end())
{
count += m[sum - k];
}
m[sum]++;
}
return count;
}
2. Leetcode523 连续子数组和
题目描述:给定一个包含 非负数 的数组和一个目标 整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,且总和为 k 的倍数,即总和为 n*k,其中 n 也是一个整数。
示例 1:
输入:[23,2,4,6,7], k = 6
输出:True
解释:[2,4] 是一个大小为 2 的子数组,并且和为 6。
思路:
本题也是一个前缀和的题目,只不过这次求得前缀和是K的倍数。因此重点就在于如何去判断当前前缀和是不是K的倍数,蛮力法?时间复杂度太高。这里要提一个平时很容易忽视的点,模运算。
如果一个数i是K的倍数,那么有 i ( m o d k ) = 0 i(mod k)=0 i(modk)=0;
如果两个数之差是K的倍数,那么有 ( j − i ) m o d k = 0 ( j- i)mod k=0 (j−i)modk=0,即 i ( m o d k ) = j ( m o d k ) i(mod k)= j(mod k) i(modk)=j(modk)。
那么把 j j j看作是前缀和 p r e [ j ] pre[j] pre[j],把 i i i看作是前缀和 p r e [ i ] pre[i] pre[i],如果两个前缀和模K后结果相同,证明它们之间的子数组和为K的倍数。
算法步骤:
-
计算所有前缀和模K的结果
-
找寻两个相同的前缀和,且其包含的子数组数目大于2。依然使用哈希表,这次哈希表存储当前位置前缀和对应的下标。
代码:
bool checkSubarraySum(vector<int>& nums, int k)
{
int len = nums.size();
if(len<=1) return false;
unordered_map<int,int> m;
// 第一个参数存储presum%k 第二个参数存储下标
m[0]=-1; //没有开始前,前缀和为0,下标为-1
int presum = 0;
for(int i=0;i<len;i++)
{
presum += nums[i];
//提前将结果模k,不影响结果但是可以加速过程,同时处理了K = 0时的特殊情况
if(k!=0) presum = presum % k;
if(m.count(presum)==1)
{
if(i-m[presum]>1)
return true;
//如果前缀余数相同,但是长度为1,不必更新下标,因为子数组越长,越容易满足题目要求
}
else
{
m[presum] = i;
}
}
return false;
}
3. Leetcode1371 每个元音字母包含偶数次的最长子字符串
**题目描述:**给你一个字符串 s
,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’ ,在子字符串中都恰好出现了偶数次。
示例 :
输入:s = “eleetminicoworoep”
输出:13
解释:最长子字符串是 “leetminicowor” ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。
思想:
要求寻找一个子串,其中所有元音字母出现的次数均为偶数次。如何统计一个子串里面的一个元音字母个数?前缀和。如何统计所有元音字母个数?二维前缀和数组 p r e [ 5 ] [ j ] pre[5][j] pre[5][j],前一个维度代表元音字母。问题就转化为求解所有的满足 ( p r e [ 5 ] [ j ] − p r e [ 5 ] [ i ] ) m o d 2 = = 0 (pre[5][j]-pre[5][i] )mod 2 == 0 (pre[5][j]−pre[5][i])mod2==0 时的 m a x ( j − i ) max(j-i) max(j−i)
算法步骤:
- 记录每个位置的元音字母的前缀和
- 根据前缀和寻找最大的满足题意的子串长度
**优化:**程序的基本思想已经写好了,但是搜寻过程并不是那么直接,蛮力做的话需要遍历很多次,做很多判断才行。根据以前的优化思路,我们需要记录状态。那么这一题要如何记录状态呢?
如果一个区间内的某个元音字母出现的次数为偶数次,这个区间两个端点的前缀和之差模2的结果相同
如果一个区间内所有元音字母出现的次数为偶数次,这个区间两个端点的所有元音字母前缀和应该相同。
把所有元音字母前缀和当作一个状态记录下来,如果存在两个相同状态的前缀和,那么就证明对应区间满足题意。
奇数模2为1,偶数模2为0,因此可以用一个五位二进制来记录前缀和的状态。最多有32个状态需要存储,与之前相比需要的空间大大减小。
代码
int findTheLongestSubstring(string s)
{
int ans = 0, status = 0, n = s.length();
vector<int> pos(1 << 5, -1);
pos[0] = 0;
for (int i = 0; i < n; i++)
{
if (s[i] == 'a')
{
status ^= 1 << 0;
}
else if (s[i] == 'e')
{
status ^= 1 << 1;
}
else if (s[i] == 'i')
{
status ^= 1 << 2;
}
else if (s[i] == 'o')
{
status ^= 1 << 3;
}
else if (s[i] == 'u')
{
status ^= 1 << 4;
}
// 出现重复状态
if(~pos[status])
{
ans = max(ans, i + 1 - pos[status]);
}
// 没有出重复状态就把最早的状态记录在内
else
{
pos[status] = i + 1;
}
}
return ans;
}