🔥你好我是fengxin_rou这是我的个人主页 fengxin_rou的主页
❄️欢迎查看我的专栏我的专栏
《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWeb+AI的talis学习系统》、《苍穹外卖》

目录
[1.1 问题背景与核心痛点](#1.1 问题背景与核心痛点)
[1.2 算法原理:字符计数 + 窗口滑动](#1.2 算法原理:字符计数 + 窗口滑动)
[1.3 完整 Java 代码实现](#1.3 完整 Java 代码实现)
[1.4 关键细节与避坑指南](#1.4 关键细节与避坑指南)
[二、前缀和算法:统计和为 K 的子数组](#二、前缀和算法:统计和为 K 的子数组)
[2.1 问题背景与核心痛点](#2.1 问题背景与核心痛点)
[2.2 算法原理:前缀和定义 + 哈希表计数](#2.2 算法原理:前缀和定义 + 哈希表计数)
[2.3 完整 Java 代码实现](#2.3 完整 Java 代码实现)
[2.4 关键细节与避坑指南](#2.4 关键细节与避坑指南)
[3.1 核心特性对比](#3.1 核心特性对比)
[3.2 实际应用场景](#3.2 实际应用场景)
[4.1 滑动窗口优化](#4.1 滑动窗口优化)
[4.2 前缀和优化](#4.2 前缀和优化)
前言
在算法面试与刷题中,字符串匹配与子数组求和是高频考点。本文将详解两道典型题:字母异位词匹配与子数组和统计,带你掌握滑动窗口与前缀和两大核心算法,高效解决这类问题。
一、滑动窗口算法:找到字符串中所有字母异位词
1.1 问题背景与核心痛点
LeetCode 438 题要求在字符串s中找到所有与字符串p的异位词匹配的子串起始索引。 异位词指由相同字母以不同顺序排列组成的字符串,传统暴力匹配会因重复统计字符而效率低下,滑动窗口是最优解。
1.2 算法原理:字符计数 + 窗口滑动
滑动窗口的核心思想是维护一个与p长度相同的窗口,通过字符计数数组统计窗口内字母出现次数,再与p的计数对比,实现 O (n) 时间复杂度匹配。
- 边界判断 :若
s长度小于p,直接返回空列表,避免无效计算。 - 初始化计数 :用两个长度为 26 的数组,分别统计
p和s中首个窗口的字母出现次数。 - 初始窗口校验:对比两个计数数组,若相同则将索引 0 加入结果。
- 窗口滑动:每次移动窗口,减去移出字符的计数,增加移入字符的计数,再校验数组是否匹配。
1.3 完整 Java 代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length(), pLen = p.length();
// 边界条件:s长度小于p,不可能存在异位词
if (sLen < pLen) {
return new ArrayList<>();
}
List<Integer> ans = new ArrayList<>();
// 统计s和p中字母出现次数(仅小写字母)
int[] sCount = new int[26];
int[] pCount = new int[26];
// 初始化第一个窗口的计数
for (int i = 0; i < pLen; ++i) {
++sCount[s.charAt(i) - 'a'];
++pCount[p.charAt(i) - 'a'];
}
// 检查初始窗口是否匹配
if (Arrays.equals(sCount, pCount)) {
ans.add(0);
}
// 滑动窗口:每次移动一位,更新计数并校验
for (int i = 0; i < sLen - pLen; ++i) {
// 移出窗口左侧字符
--sCount[s.charAt(i) - 'a'];
// 移入窗口右侧新字符
++sCount[s.charAt(i + pLen) - 'a'];
// 校验当前窗口是否匹配异位词
if (Arrays.equals(sCount, pCount)) {
ans.add(i + 1);
}
}
return ans;
}
}
1.4 关键细节与避坑指南
- 字符计数优化 :使用长度为 26 的数组代替哈希表,利用字母与
'a'的偏移直接索引,效率更高。 - 数组对比效率 :
Arrays.equals直接对比两个计数数组,避免手动遍历,代码更简洁。 - 窗口移动边界 :循环终止条件为
sLen - pLen,确保窗口不会超出字符串s的范围。
二、前缀和算法:统计和为 K 的子数组
2.1 问题背景与核心痛点
LeetCode 560 题要求统计数组中连续子数组和等于k的个数。 数组中存在负数时,滑动窗口的单调性失效,无法直接使用;暴力枚举子数组则时间复杂度为 O (n²),效率低下,前缀和 + 哈希表是最优解。
2.2 算法原理:前缀和定义 + 哈希表计数
前缀和指数组前i个元素的和,定义preSum[i]为前i个元素的和,子数组[j, i]的和可表示为preSum[i+1] - preSum[j]。 要使子数组和为k,即preSum[i+1] - preSum[j] = k,等价于preSum[j] = preSum[i+1] - k。 用哈希表记录每个前缀和出现的次数,遍历前缀和时统计满足条件的preSum[j]数量即可。
2.3 完整 Java 代码实现
import java.util.HashMap;
class Solution {
public int subarraySum(int[] nums, int k) {
// 前缀和数组,preSum[0] = 0,preSum[i]为前i个元素的和
int[] preSum = new int[nums.length + 1];
preSum[0] = 0;
for (int i = 0; i < nums.length; i++) {
preSum[i + 1] = preSum[i] + nums[i];
}
int ans = 0;
// 哈希表:key为前缀和,value为该前缀和出现的次数
HashMap<Integer, Integer> preSumCount = new HashMap<>();
for (int sj : preSum) {
// 寻找满足 si = sj - k 的前缀和次数
int si = sj - k;
if (preSumCount.containsKey(si)) {
ans += preSumCount.get(si);
}
// 将当前前缀和加入哈希表,更新计数
preSumCount.put(sj, preSumCount.getOrDefault(sj, 0) + 1);
}
return ans;
}
}
2.4 关键细节与避坑指南
- 前缀和初始化 :
preSum[0] = 0是关键,处理从数组开头到当前位置的子数组和为k的情况。 - 哈希表更新顺序 :先查询
si = sj - k的次数,再将当前前缀和sj加入哈希表,避免重复统计。 - 处理负数与 0:前缀和 + 哈希表天然支持数组中存在负数、0 的场景,无需额外处理。
三、两大算法对比与应用场景分析
3.1 核心特性对比
表格
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 滑动窗口 | O(n) | O (1)(固定长度窗口) | 字符串匹配、定长子数组问题,元素为正数或有固定窗口长度 |
| 前缀和 + 哈希表 | O(n) | O(n) | 不定长子数组和问题,含负数、0 的数组,无法用滑动窗口的场景 |
3.2 实际应用场景
- 滑动窗口:可扩展到最长无重复子串、最小覆盖子串等字符串问题,也可用于定长滑动窗口的统计类问题。
- 前缀和:广泛应用于数组区间和查询、子数组统计问题,结合哈希表可高效解决带条件的子数组计数。
四、性能优化与扩展思考
4.1 滑动窗口优化
在字母异位词问题中,可进一步优化:
- 用一个变量记录窗口内与
p字符计数不同的字母数量,避免每次对比数组,将常数时间进一步降低。 - 当
s中出现非小写字母时,可扩展计数数组大小,或用哈希表兼容所有字符。
4.2 前缀和优化
在子数组和问题中,可直接在遍历数组时计算前缀和并更新哈希表,无需额外创建前缀和数组,节省空间:
public int subarraySum(int[] nums, int k) {
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
int preSum = 0, ans = 0;
for (int num : nums) {
preSum += num;
ans += map.getOrDefault(preSum - k, 0);
map.put(preSum, map.getOrDefault(preSum, 0) + 1);
}
return ans;
}
结语
滑动窗口与前缀和是解决字符串匹配与子数组问题的核心算法。滑动窗口通过维护固定窗口实现高效字符统计,适用于定长匹配场景;前缀和 + 哈希表则解决了含负数的子数组和问题,是不定长区间问题的通用方案。
掌握这两种算法的核心原理与实现细节,能大幅提升刷题效率与面试竞争力。后续可进一步学习滑动窗口的可变长度实现、二维前缀和等扩展应用,深入理解算法思想的本质。


