Google, Facebook高频面试题 | Subarray Sum Equals K

先一起来看看题目:

题目描述

Given an array of integers and an integer k, you need to find the total number of contiguous subarrays whose sum equals to k.

Example 1:

Input: nums = [1,1,1], k = 2Output: 2

[1, 1, 1]

[1, 1, 1]

考点解析

在面试中我们经常会碰到一类问题,跟input array中的所有subarray sum相关。

在这类题目中,我们可以使用到一个非常常见的precomputation的方法来进行优化:prefixSum array。

今天的习题讲解,我们就重点讲解一下:

如何利用prefixSum array的思想,非常有效的解决Subarray Sum Equals K这个问题。

接下来,我们有请闫老师为我们详解这道题的答案:

主讲人:闫老师

来Offer金牌老师

前Google资深程序员、面试官

Google连续多年top performer


解题思路

在这道题目中,闫老师讲解了三个不同的解法,从最naive的解法,到optimize的解法。并一步步讲解本题的优化过程

解法1:Naive Solution

拿到题目,我们很快就能想到的一个最直接的方法:

找到input array的所有可能的subarray,然后判断每个subarray的和是否与k相等。

一个比较暴力的方法,就是用三层for loop,找到所有可能的subarray,并遍历subarray中的每个数,计算subarray的和。

解法2:Better Solution

从上一个解法出发,我们能看到一个比较明显的优化就是减少重复计算

在Naive的解法中,在计算subarray的和的时候,我们每次都需要遍历该subarray中的每个数。实际上,这是有很多重复的。

以subarray(i, j)为例(i为subarray的起点,j为终点):

当i = 0,j = 1的时候,需要遍历nums[0], nums[1]

当i = 0, j = 2的时候,需要遍历nums[0], nums[1]nums[2]

显然,前两个数字被遍历了多次。图中蓝色部分为重复遍历的元素。

那么,减少重复的一个方法,就是把之前已经计算过的subarray(i, j)的和保存下来,当j++的时候我们只需要在原来的和的基础上加上新的nums[j],来达到减少重复遍历的目的,也降低了时间复杂度。

解法3:Linear Solution

进一步优化,我们会用到一个非常重要的precomputation的技巧:prefixSum array。

prefixSum array的每一个index所对应的value,和最开始的input array有一个对应关系:

通过这个prefixSum array,我们就可以用O(1)的时间求出任意一个subarray的和:

sum of subarray(i, j) = prefixSum[j] - prefixSum[i - 1]

注意上图中红色的0,是为了解决当i == 0的时候的subarray(i, j)的和,这时对应的prefixSum[i - 1]应该是0

有了prefixSum array,这个问题就转化成了

for each j:how many i < j satisfies prefixSum[i] = prefixSum[j] - k

这意味着对于每个j,我们需要记录之前所有的prefixSum,然后在这些prefixSum中查找有多少个数值 == prefixSum[j] - k。

这是一个look up的操作,为了更加有效的解决这个问题,我们可以使用一个HashMap:

key:prefixSum value

value:number of occurence of the prefixSum value。

在实际实现中,我们发现,在从左到右遍历整个array过程中,我们可以只需要一个prefixSum的variable来依次计算所有的prefixSum值,并将其存入HashMap中,无需提前计算出整个prefixSum array。

代码实现

算法与代码中需要注意的细节处理及时间、空间复杂度分析请观看视频讲解。

E/N/D


更多科技求职资讯,请关注“来Offer”


你给出的函数签名: ```cpp int subarraySumEqualsK(vector<int> &nums, int K) ``` 是 **C++** 的语法(使用 `vector`),但你要求用 **C 语言** 实现 LintCode 838:求和为 `k` 的连续子数组个数。 --- ## ✅ 目标 我们使用 **纯 C 语言** 实现: - 输入:整型数组 `nums`,长度 `n`,目标和 `k` - 输出:连续子数组中和等于 `k` 的个数 例如: ```c nums = [2, 1, -1, 1, 2], k = 3 → 子数组:[2,1], [2,-1,1,2], [2,1,-1,1], [1,2] → 共 4 个 ``` --- ## ✅ 解法一:前缀和 + 哈希表(推荐,O(n) 时间) ### 🔍 思路 定义前缀和 `prefix[i] = nums[0] + nums[1] + ... + nums[i-1]` 若 `prefix[j] - prefix[i] == k`,则 `nums[i..j-1]` 的和为 `k` 即:我们要找有多少对 `(i, j)` 满足 `prefix[j] - prefix[i] == k` 等价于:遍历过程中,对当前前缀和 `sum`,查找之前是否有前缀和为 `sum - k` 的情况。 我们可以用哈希表记录每个前缀和出现的次数。 但由于 C 没有内置 `hashmap`,我们需要自己实现一个简单的 **哈希表(链地址法)**。 --- ## ✅ C 语言完整实现(前缀和 + 哈希表) ```c #include <stdio.h> #include <stdlib.h> // 哈希表节点(用于存储前缀和及其频次) typedef struct HashNode { int key; // 前缀和的值 int count; // 出现次数 struct HashNode* next; // 链地址法解决冲突 } HashNode; // 哈希表结构 typedef struct { int size; HashNode** buckets; } HashMap; // 创建哈希表 HashMap* createHashMap(int size) { HashMap* map = (HashMap*)malloc(sizeof(HashMap)); map->size = size; map->buckets = (HashNode**)calloc(size, sizeof(HashNode*)); return map; } // 哈希函数 int hash(int key, int size) { return (key < 0 ? -key : key) % size; // 处理负数 } // 插入或更新:key 出现次数 +1 void hashMapPut(HashMap* map, int key) { int index = hash(key, map->size); HashNode* head = map->buckets[index]; // 查看是否已存在该 key while (head != NULL) { if (head->key == key) { head->count++; return; } head = head->next; } // 不存在,插入新节点 HashNode* newNode = (HashNode*)malloc(sizeof(HashNode)); newNode->key = key; newNode->count = 1; newNode->next = map->buckets[index]; map->buckets[index] = newNode; } // 获取 key 的出现次数 int hashMapGet(HashMap* map, int key) { int index = hash(key, map->size); HashNode* head = map->buckets[index]; while (head != NULL) { if (head->key == key) { return head->count; } head = head->next; } return 0; } // 释放哈希表内存 void freeHashMap(HashMap* map) { for (int i = 0; i < map->size; i++) { HashNode* head = map->buckets[i]; while (head != NULL) { HashNode* temp = head; head = head->next; free(temp); } } free(map->buckets); free(map); } /** * 求连续子数组和为 k 的个数 * @param nums: 整数数组 * @param n: 数组长度 * @param k: 目标和 * @return: 满足条件的子数组个数 */ int subarraySumEqualsK(int* nums, int n, int k) { if (!nums || n == 0) return 0; // 创建哈希表,初始大小设为 100(可调整) HashMap* map = createHashMap(100); // 初始化:前缀和为 0 的情况出现 1 次 hashMapPut(map, 0); int sum = 0; int count = 0; for (int i = 0; i < n; i++) { sum += nums[i]; // 当前前缀和 // 查询是否存在前缀和为 (sum - k) int target = sum - k; count += hashMapGet(map, target); // 将当前前缀和存入哈希表 hashMapPut(map, sum); } freeHashMap(map); return count; } // ============ 测试代码 ============ int main() { int nums[] = {2, 1, -1, 1, 2}; int n = sizeof(nums) / sizeof(nums[0]); int k = 3; int result = subarraySumEqualsK(nums, n, k); printf("Input: ["); for (int i = 0; i < n; i++) { if (i > 0) printf(", "); printf("%d", nums[i]); } printf("], k = %d\n", k); printf("Output: %d\n", result); // 应输出 4 return 0; } ``` --- ## ✅ 输出结果 ``` Input: [2, 1, -1, 1, 2], k = 3 Output: 4 ``` ✅ 正确! --- ## ✅ 算法原理详解 | 步骤 | 说明 | |------|------| | 1. 前缀和 | `sum` 表示从 `0` 到 `i` 的累加和 | | 2. 哈希表记录 | 记录每个前缀和出现的次数 | | 3. 关键逻辑 | 若 `sum - k` 在之前出现过,则说明存在某个 `j < i` 使得 `sum[i] - sum[j] == k` → 区间 `[j+1, i]` 和为 `k` | | 4. 初始状态 | `hashMapPut(map, 0)`:表示空前缀和出现一次,处理从下标 0 开始的子数组 | --- ## ✅ 示例分析(nums = [2,1,-1,1,2], k=3) | i | num | sum(前缀和) | sum-k | hashMap 中是否有? | count += | |---|-----|----------------|--------|----------------------|----------| | 0 | 2 | 2 | -1 || 0 | | 1 | 1 | 3 | 0 | 是(初始插入) | +1 → 1 | | 2 | -1 | 2 | -1 || 0 | | 3 | 1 | 3 | 0 | 是(出现过) | +1 → 2 | | 4 | 2 | 5 | 2 | 是(sum=2 出现过两次)| +2 → 4 | 最终 `count = 4` --- ## ✅ 时间与空间复杂度 - **时间复杂度**:O(n),单次遍历,哈希操作平均 O(1) - **空间复杂度**:O(n),哈希表最多存 n 个不同前缀和 --- ## ✅ 对比暴力解法(O(n²)) 也可以用两层循环枚举所有子数组: ```c int subarraySumEqualsK_bruteforce(int* nums, int n, int k) { int count = 0; for (int i = 0; i < n; i++) { int sum = 0; for (int j = i; j < n; j++) { sum += nums[j]; if (sum == k) { count++; } } } return count; } ``` 虽然简单,但时间复杂度 O(n²),大数据会超时。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值