leetcode 前缀和[&哈希表优化]
前缀和(Prefix Sum)定义:前缀和是一种重要的预处理,能大大降低查询的时间复杂度。结合Hash缓存,能够进一步优化提升算法执行效率。
对数组nums进行前缀和初始化需要O(n)时间。
新建数组prefixSum,数组长度定义为nums.length+1,确保第nums.length个元素存储了前面0到nums.length-1个元素的和。将数组nums的累加依次放入数组prefixSum中
prefixSum [0] = 0 【备注:此处不要定义prefix[0]=nums[0],这种定义违背前缀和的意义】
prefixSum [1] = prefixSum[0] + nums[0] = 0 + num[0]
…
prefixSum [i] = prefixSum [i-1] + nums [i]。
由此推导出两个变换公式:
(1)nums[某一项] = 两个相邻 前缀和 之差:
nums[x] = prefixSum[x] - prefixSum[x - 1]
(2)从left到right的元素和等于prefixSum[right+1] –prefixSum[left];
前缀和的应用场景是,需要对某个区间[i…j]频繁查询累计和,避免每次查询都遍历这个区间。
leetcode560. 和为K的子数组
1. 题目
给定一个整数数组和一个整数k,你需要找到该数组中和为k的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :
数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。
2. 解答
/*
易错点:
1.每次循环前应该是先搜索再存sum,否则当k = 0时,将搜索到当前位置
2.循环开始前应该先存个0进去。因为当前n个元素的和sum = k时,其中有一个结果应该是这n个元素本身
*/
typedef struct {
int preSum; /* we'll use this field as the key */
int count;
UT_hash_handle hh; /* makes this structure hashable */
}hash_node;
int subarraySum(int* nums, int numsSize, int k) {
int sum = 0;
int needPresum;
int count = 0;
hash_node *g_users = NULL;
hash_node *add_user = (hash_node *)malloc(sizeof(hash_node));
add_user->preSum = 0;
add_user->count = 1;
HASH_ADD_INT(g_users, preSum, add_user);
hash_node *find_user = NULL;
for (int i = 0; i < numsSize; i++) {
sum += nums[i];
needPresum = sum - k;
HASH_FIND_INT(g_users, &needPresum, find_user);
if (find_user != NULL) {
count += find_user->count;
}
HASH_FIND_INT(g_users, &sum, find_user);
if (find_user == NULL) {
hash_node *new_user = (hash_node *)malloc(sizeof(hash_node));
new_user->preSum = sum;
new_user->count = 1;
HASH_ADD_INT(g_users, preSum, new_user);
} else {
find_user->count++;
}
}
return count;
}
leetcode523. 连续的子数组和
1. 题目
给定一个包含 非负数 的数组和一个目标 整数 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。
2. 解答
/*
我们假设preSum[i]表示nums[0] + ... + nums[i]的和,那么当preSum[j] - preSum[i]刚好为n * k,即k的倍数时,preSum[i] % k == preSum[j] % k这个关系表达式应该成立;或者可以看做preSum[j] = preSum[i] + n * k;
那我们以preSum[i] % k作为哈希索引的key值,i索引作为哈希表的value值;就可以在索引到相同key值且索引相差至少为2的情况下,返回true
*/
struct UtHashStr {
int preSum; /* key */
int num; /* value */ // 由于返回值不是统计个数,统计的是长度大于等于2的数组是否存在等于K,此处表示数组下标的索引值
UT_hash_handle hh; /* makes this structure hashable! */
};
bool checkSubarraySum(int* nums, int numsSize, int k) {
struct UtHashStr *hash = NULL; /* 就理解为头节点吧 */
struct UtHashStr *s = (struct UtHashStr*)malloc(sizeof(struct UtHashStr));
s->preSum = 0;
s->num = -1;
/* 这里将[0, -1]键值对插入,为什么0对应的是-1呢?因为0对应的实际上是总和刚好为k倍数的值,而题目要求连续子数组大小至少为2,所以这里初始化为-1 */
HASH_ADD_INT(hash, preSum, s);
int sum = 0;
for (int i = 0; i < numsSize; i++) {
sum += nums[i];
if (k != 0) {
sum = sum % k;
}
struct UtHashStr *tmp;
HASH_FIND_INT(hash, &sum, tmp); /* 如果两数对k取余数相等,那么这两数相差为k的倍数,所以看对k取余数的哈希值,如果相等,则判断两者索引相差 */
if (tmp == NULL) {
tmp = (struct UtHashStr*)malloc(sizeof(struct UtHashStr));
tmp->preSum = sum;
tmp->num = i;
HASH_ADD_INT(hash, preSum, tmp);
} else {
if (i - (tmp->num) > 1) { /* 如果两数对k取余数哈希值相等,且索引相差至少为2(hash[0] = -1初始化),则存在连续子数组并返回true */
return true;
}
}
}
return false;
}
leetcode974. 和可被K整除的子数组
1. 题目
给定一个整数数组 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]
2. 解答
/*
以preSum[i]表示nums[0]+...+nums[i]这一段的前缀和,那如果preSum[i] % k == m; preSum[j] % k == m; 那nums[i + 1]到nums[j]的和可以被k整除,这一段是符合题目要求的;
如果在首次遇见preSum[i] % k == m时,将其放入哈希表,以m余数为key值,并将其value值初始化为1;
则当第二次遇见preSum[j] % k == m时(j > i),搜索哈希表发现key为m的已经存在,那nums[i + 1]到nums[j]这一段就是余数为m为起点的第一段符合题目意思的连续子数组,这时我们更新result += key(m).value,key == m的value值更新为 1 + 1 = 2;
我们继续看,当第三次遇见preSum[p] % k == m时(p > j), 搜索哈希发现key为md的节点已经存在,那nums[j + 1] -> nums[k]这一段就是余数为m为起点的“第二段”符合题目意思的连续子数组,但是这时候我们发现其实nums[i + 1] -> nums[k]这一段也符合题意,所以我们继续更新result += key(m).value, key == m的value值同步更新为 2 + 1 = 3;以此类推。。。
所以,相同余数(key)的哈希值,每多一个段,这一段可以与之前存在的段都组成新的子数组段,所以result += key(m).value
*/
//注意:此题是整数,66是非负整数,索引值preSum[i] % k即可,但此题要区分preSum[i]是正数还是负数,索引值应为索引值(preSum[i] % k + k) % k
typedef struct hash_node {
int preSum;
int count;
UT_hash_handle hh;
} UTHash;
int subarraysDivByK(int* A, int ASize, int K) {
int sum = 0;
int count = 0;
UTHash *g_hash_node = NULL;
UTHash *cur_node = (UTHash *)malloc(sizeof(UTHash));
cur_node->preSum = 0;
cur_node->count = 1;
HASH_ADD_INT(g_hash_node, preSum, cur_node);
for (int i = 0; i < ASize; i++) {
sum += A[i];
sum = (sum % K + K) % K;
UTHash *find_node = NULL;
HASH_FIND_INT(g_hash_node, &sum, find_node);
if (find_node == NULL) {
UTHash *cur_node = (UTHash *)malloc(sizeof(UTHash));
cur_node->preSum = sum;
cur_node->count = 1;
HASH_ADD_INT(g_hash_node, preSum, cur_node);
} else {
count += find_node->count;
find_node->count++;
}
}
return count;
}
leetcode525. 连续数组
1. 题目
给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:
输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
提示:
1 <= nums.length <= 105
nums[i] 不是 0 就是 1
2. 解答
// 前缀和思想
// 1. 将数组中的0替换为-1, 子数组0/1个数相同转为子数组求和为0;
// 2. 利用前缀和和hash表实现, 如果preSum[i] == preSum[j], 则说明子数组和[i + 1, j]为0
typedef struct {
int key;
int value;
UT_hash_handle hh;
} NumHash;
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
int findMaxLength(int* nums, int numsSize) {
int sum = 0;
int res = 0;
NumHash *g_num = NULL;
NumHash *cur = (NumHash *)malloc(sizeof(NumHash));
cur->key = 0;
cur->value = -1;
HASH_ADD_INT(g_num, key, cur);
for (int i = 0; i < numsSize; i++) {
sum += nums[i] ? 1 : -1;
NumHash *find = NULL;
HASH_FIND_INT(g_num, &sum, find);
if (find != NULL) {
res = MAX(res, i - find->value);
} else {
NumHash *temp = (NumHash *)malloc(sizeof(NumHash));
temp->key = sum;
temp->value = i;
HASH_ADD_INT(g_num, key, temp);
}
}
return res;
}
剑指 Offer II 011. 0 和 1 个数相同的子数组
1. 题目
给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:
输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。
提示:
1 <= nums.length <= 105
nums[i] 不是 0 就是 1
2. 解答
typedef struct {
int key;
int value;
UT_hash_handle hh;
} hash_node;
int findMaxLength(int* nums, int numsSize) {
if (nums == NULL || numsSize <= 0) {
return 0;
}
int res = 0;
hash_node *g_user = NULL;
int sum = 0;
for (int i = 0; i < numsSize; i++) {
sum += nums[i] > 0 ? 1 : -1;
if (sum == 0) {
res = i + 1;
}
hash_node *find = NULL;
HASH_FIND_INT(g_user, &sum, find);
if (!find) {
find = (hash_node *)malloc(sizeof(hash_node));
find->key = sum;
find->value = i;
HASH_ADD_INT(g_user, key, find);
} else {
res = fmax(res, i - find->value);
}
}
return res;
}