算法总结-前缀和问题

前缀和(preSum)是一种常用的、较为高效的预处理方式,相比于暴力解法,能够有效减低查询的时间复杂度。

1、前缀和算法

前缀和其实就是数组的前n项之和,为了更好的理解和应用它,我们用一个题目来学习前缀和算法。

给你一个数组 nums 。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i]) 。

请返回 nums 的动态和。

示例 1:

输入:nums = [1,2,3,4]
输出:[1,3,6,10]
解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4] 。

提示:

  • 1 <= nums.length <= 1000
  • -10^6 <= nums[i] <= 10^6

这是一个典型的前缀和题目。如果使用暴力解法,也可以做出这道题,但需要两个for循环,时间复杂度较大,暴力解法如下:

int* runningSum(int* nums, int numsSize, int* returnSize){
    int *arr = malloc(sizeof(int) * (numsSize));
    *returnSize = numsSize;
    for(int i = 0;i < numsSize;i++) {
        int temp = 0;
        for(int j = 0;j < i+1;j++) {
            temp += nums[j];
        }
            arr[i] = temp;
    }
    return arr;
}

如果用前缀和的解法,只需要一次循环,时间复杂度会大大降低。如下。

int* runningSum(int* nums, int numsSize, int* returnSize) {
    *returnSize = numsSize;
    for (int i = 1; i < numsSize; i++) {
        nums[i] += nums[i - 1];
    }
    return nums;
}

前缀和不仅代码简洁,性能上更是优于暴力解,因此能用前缀和来解决的题目应尽量避免使用暴力解法。

2、前缀和+哈希

前缀和有时会和哈希表一起使用,用来解决例如连续子数组之类的问题。如下问题

leetcode 560.和为k的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。


示例 1:

输入:nums = [1,2,3], k = 3
输出:2
 

提示:

1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107

这类问题很明显可以用前缀和来解答,但只使用前缀和在计算量较大的情况下效率仍然较低,可能会存在超时的问题,这时可以使用哈希表来进行优化。

前缀和问题的关键是找出所有前缀和为k的子数组,这个问题可以转换为”当前前缀和 - 前面前缀和 == k“,即preSum[i]  - preSum[j] = sum[i, j] ,也即preSum[i]  - preSum[j] = k。这样我们就可以用哈希表来存储每一个子数组和,以便于查找。

那么,如果存在preSum[i]  - preSum[j] = k,就可以利用哈希表记录preSum-k的元素,查找并计算preSum-k存在的次数即为目标值的个数。

代码如下:

struct hashtable
{
    int key;
    int cnt;
    UT_hash_handle hh;
};
struct hashtable *g_head = NULL;

int subarraySum(int* nums, int numsSize, int k)
{
    g_head = NULL;
    struct hashtable *node = (struct hashtable*)malloc(sizeof(struct hashtable));
    node->key = 0;
    node->cnt = 1;
    HASH_ADD_INT(g_head, key, node);

    int sum_i = 0, ans = 0;
    for (int i = 0; i < numsSize; i++) {
        sum_i += nums[i];
        int sum_j = sum_i-k;

        struct hashtable *s;
        HASH_FIND_INT(g_head, &sum_j, s);
        if (s != NULL) {
            ans += s->cnt;
        }

        HASH_FIND_INT(g_head, &sum_i, s);  //查找当前preSum是否在哈希表,没有则将当前preSum入哈希表
        if (s == NULL) {
            s = (struct hashtable*)malloc(sizeof(struct hashtable));
            s->key = sum_i;
            s->cnt = 1;
            HASH_ADD_INT(g_head, key, s);
        } else {
            s->cnt++;
        }
    }
    return ans;
}

这个题目的关键,一是要彻底理解hash表中所存放值的含义;二是要理解返回值为什么是ans += s->cnt;而不是ans++。

我们以一个例子来理解这两个问题,假设数组为[1, 0, 3, 2, 4],所求的目标值为5,那么其前缀和如下(假设下标从1开始):

sum[1] = 1;

sum[2] = 1;

sum[3] = 4;

sum[4] = 6;

sum[5] = 10;

这几个值是要存到hash表中去的,其在hash表中的键值对为(key-value):1-2,4-1,6-1,10-1,我们到hash表中查找时,是按其key值检索的。

那么sum - k分别为:

[-4, -4, -1, 1,5]

这几个值就是我们要在哈希表中查找的,如果存在,那么就是我们的目标值,找到其对应的value即可,value就是其个数。比如以上例子我们的目标值为5,那么sum - k值为1的时候,哈希表中是有对应值的,即键值对1-2,这个就是我们要找的值,其结果为2,此时当前的前缀和为sum[4] = 6,所要减去的前缀和为sum[0]、sum[1](因为两个值都为1)。

对于第二个问题,用一个简单的例子就可以说明。一般情况下,hash表中的键值对都是x-1的形式,但有些特殊情况下例外,如数组为[1, -1, 0, 0],目标值为0的情况,这时

sum[0] = 1;

sum[1] = 0;

sum[2] = 0;

sum[3] = 0;

将各前缀和存到hash表中后,值为0的key,有3个,分别为sum[1]、sum[2]、sum[3],但在哈希表实际上占用一个键值对,即0-3。假如i = 3,那么当前前缀和为sum[3] = 0,以0 - k = 0到哈希表中查找目标值,此时会有两个值满足条件,即sum[1]、sum[2],其逻辑为,对于当前前缀和sum[3],存在前缀和sum[1]、sum[2]使得sum[3] - sum[1]或sum[2],其结果值为k。

这样的话,对于sum[3]而言,满足条件的值就不是一个了,所以计算结果时要用ans += s->cnt,而不是ans++来计算。

总结:要真正理解hash表中存的值是什么,hash表中存的是sum[0] - sum[i]的各个前缀和(不包括中间截取的情况)。而我们要拿去hash表查找的key,其实是sum-k,也就是当前前缀和之前的前缀和(如sum[3]之前的前缀和sum[1]或sum[2]),如果这个值存在hash表中,就说明条件成立。

解决这类问题要注意问题的转变,比如前面题目中我们将前缀和的问题转换为了hash表中寻找sum-k的问题,这时我们的问题已经转变,不再是求子数组的个数,而是求hash表中sum-k的个数,当问题转变时,条件也会转变,切不要将转变后的问题和条件与原来的问题和条件相混淆。

最后还应该释放哈希表。

    HASH_ITER(hh, g_head, current, next) {
        if (current) {
            HASH_DEL(g_head, current);
        }
    }

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值