01_fixed_slide_win.md 定长滑动窗口刷题笔记(C语言完整版)
代码链接:https://gitee.com/hiwangGitee/0x3f-algorithm-c/tree/main/
通用模板核心
定长滑动窗口通用公式 :left = i - k + 1
通用执行流程:元素入窗统计 → 窗口满长更新答案 → 滑出左边界平移窗口
定长滑动窗口专题总结
通用核心公式 :left = i - k + 1,所有题目统一窗口逻辑,仅内部统计逻辑不同
题型分类:
-
数值求和/最值:643、1052,适合数组区间统计;
-
字符计数最值:1456、2379,简单字符统计窗口;
-
频次匹配类:567、438,26字母桶匹配异位词;
-
字符串哈希查重:187,固定长度子串去重统计。
模板复用性:全部题目遵循「入窗统计 → 窗口满更新答案 → 出窗平移」流程,可直接作为刷题万能模板。
全局公共头文件
c
#include <stdio.h> // 标准输入输出,printf/scanf必备
#include <stdlib.h> // malloc/free、exit()、rand()
#include <string.h> // 字符串函数 strlen/strcpy/memset
#include <ctype.h> // 字符判断 isdigit/isalpha
#include <math.h> // 数学函数 sqrt/sin/cos
#include <time.h> // 时间相关 time()
#include <stdbool.h> // C99 布尔类型 bool true/false
#include <limits.h>
#include "uthash.h"
643 子数组最大平均数 I
题目链接:https://leetcode.cn/problems/maximum-average-subarray-i/
题目描述
给你一个由 n 个元素组成的整数数组 nums 和一个整数 k。
请你找出平均数最大、且长度为 k 的连续子数组,输出该最大平均数。
答案误差小于10^-5 即判定正确。
示例
示例1 输入:nums = [1,12,-5,-6,50,3], k = 4输出:12.75 解释:最优子数组和 KaTeX parse error: Undefined control sequence: \- at position 3: 12\̲-̲5\-6\+50=51,平均数 51/4=127˙551/4=12\.7551/4=127˙5
示例2 输入:nums = [5], k = 1 输出:5.00000
核心考点
定长滑动窗口入门模板、增量维护窗口和、线性时间遍历优化
1. 公式含义
left = i - k + 1
当前右边界下标i,窗口固定长度k,窗口左边界下标 = 右边界 - 窗口长度 + 1
举例:i=3、k=4(样例 1 第 4 个元素),left=3-4+1=0,窗口范围0,3,正好 4 个元素,完全匹配定长窗口区间。
2. 分阶段作用
阶段 1:left < 0(窗口元素不足 k 个)
只累加元素、不做均值计算,跳过滑出逻辑,完成初始窗口填充;
阶段 2:left ≥ 0(窗口凑满 k 个元素)
立刻计算当前窗口平均数,对比更新最大值;随后剔除numsleft、窗口整体右移一格,开启下一轮迭代。
代码实现
c
double findMaxAverage(int *nums, int numsSize, int k)
{
double res = 0, result = INT_MIN;
double cnt = 0;
int num = 0;
for (int i = 0; i < numsSize; i++) {
cnt = cnt + nums[i];
num = num + 1;
// 当前右边界下标i,窗口固定长度k,窗口左边界下标 = 右边界 - 窗口长度 + 1
int left = i - k + 1;
if (left < 0) {
continue;
}
res = cnt / num;
if (res > result) {
result = fmax(res, result);
}
cnt = cnt - nums[left];
num = num - 1;
}
return result;
}
1456 定长子串中元音的最大数目
题目链接:https://leetcode.cn/problems/maximum-number-of-vowels-in-a-substring-of-given-length/
题目描述
给定字符串 s 和整数k,找出 s 所有长度固定为 k 的连续子串里,包含元音字母(a、e、i、o、u)数量的最大值并返回。
输入输出示例
示例1 输入:s = "abciiidef", k = 3 输出:3 解释:子串 "iii" 包含3个元音字母,为全局最大值
示例2 输入:s = "aeiou", k = 2 输出:2 解释:任意长度为2的子串均包含2个元音字母
示例3 输入:s = "leetcode", k = 3 输出:2 解释:"lee"、"eet"、"ode" 这类子串元音数为当前最大的2
示例4 输入:s = "rhythms", k = 4 输出:0 解释:整串无元音字母,结果为0
示例5 输入:s = "tryhard", k = 4 输出:1
核心考点
定长滑动窗口基础应用,用通用公式 left = i - k + 1 控制窗口边界,增量维护窗口内元音计数,线性遍历求解全局最大值
代码实现
c
bool isVowel(char c)
{
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}
int maxVowels(char *s, int k)
{
int len = strlen(s);
int windowCnt = 0;
int maxCnt = 0;
for (int i = 0; i < len; i++) {
if (isVowel(s[i])) {
windowCnt++;
}
int left = i - k + 1;
if (left < 0) {
continue;
}
if (windowCnt > maxCnt) {
maxCnt = windowCnt;
}
if (isVowel(s[left])) {
windowCnt--;
}
}
return maxCnt;
}
1052 爱生气的书店老板
题目链接:https://leetcode.cn/problems/grumpy-bookstore-owner/
题目描述
书店营业共 n 分钟:
-
customers[i]:第i分钟到店顾客数; -
grumpy[i]=1:老板该分钟生气,顾客不满意;grumpy[i]=0:老板不生气,顾客满意;
老板可使用1次技能,连续 minutes 分钟强制不生气,原本生气时段的顾客也会满意。求全天最多能收获的满意顾客总数。
输入输出示例
示例1 输入:customers = 1,0,1,2,1,1,7,5,grumpy = 0,1,0,1,0,1,0,1,minutes = 3 输出:16 解释:技能作用于最后3分钟,原本不生气时段顾客照常满意,原本生气的3段顾客被额外挽救,累加得到最大值16
示例2 输入:customers = 1, grumpy = 0, minutes = 1 输出:1 解释:原本老板就不生气,技能不改变结果
解题思路拆解
-
基础满意值求和:先累加所有
grumpy[i]=0位置的顾客,这部分不受技能影响; -
定长滑窗求增量最大值:窗口长度固定为
minutes,只统计窗口内grumpy[i]=1的顾客和(使用技能能额外挽救的人数),用left = i - minutes + 1经典定长窗口公式遍历; -
最终答案 = 基础满意值 + 最大可挽救增量。
核心考点
定长滑动窗口求区间最值、拆分固定收益+窗口浮动增量的两步解题模型,线性时间遍历优化
代码实现
c
int maxSatisfied(int *customers, int customersSize, int *grumpy, int grumpySize, int minutes)
{
int max_s = 0;
int base = 0, add_cnt = 0;
for (int i = 0; i < customersSize; i++) {
if (grumpy[i] == 0) {
base += customers[i];
}
}
// 选一段长度严格等于 minutes 的连续区间,
// 区间内原本grumpy[i]=1(生气流失)的顾客,全部变成满意顾客
for (int i = 0; i < customersSize; i++) {
if (grumpy[i] == 1) {
add_cnt += customers[i]; // 增益值,益值 = 窗口内原本生气(grumpy[i]=1)的顾客数
}
int left = i - minutes + 1; // 窗口左端点
if (left < 0) { // 窗口长度不足 minutes
continue;
}
max_s = fmax(max_s, add_cnt);
if (grumpy[left] == 1) {
add_cnt -= customers[left];
}
}
return base + max_s;
}
2379 得到 K 个黑块的最少涂色次数
题目链接:https://leetcode.cn/problems/minimum-recolors-to-get-k-consecutive-black-blocks/
题目描述
给定长度为n、仅由W(白色块)和B(黑色块)组成的字符串blocks,每次操作可把白色块涂成黑色块。要求字符串中至少出现一段连续k个黑色块,求需要的最少涂色次数。
输入输出示例
示例1 输入:blocks = "WBBWWBBWBW", k = 7 输出:3 解释:选取一段长度7的区间,将区间内3个白块涂黑即可达成目标,不存在操作次数更少的方案
示例2 输入:blocks = "WBWBBBW", k = 2 输出:0 解释:原字符串本身就存在连续2个黑块,无需涂色操作
解题思路拆解
-
问题等价转化:任意一段长度固定为
k的连续区间,区间内白色块数量就是把这段全部涂黑需要的操作次数;题目求全局最小值; -
采用定长滑动窗口遍历所有长度
k的区间,用通用边界公式left = i - k + 1维护窗口; -
右指针入窗统计白块数量,窗口满长后更新最小操作数,再滑出左边界元素完成平移。
核心考点
定长滑动窗口计数求全局最小值,入门级定长窗口模板应用题
代码实现
c
int minimumRecolors(char *blocks, int k)
{
int len = strlen(blocks);
int windowWhite = 0;
int minOp = k;
for (int i = 0; i < len; i++) {
if (blocks[i] == 'W') {
windowWhite++;
}
int left = i - k + 1;
if (left < 0) {
continue;
}
if (windowWhite < minOp) {
minOp = windowWhite;
}
if (blocks[left] == 'W') {
windowWhite--;
}
}
return minOp;
}
567 字符串的排列
题目链接:https://leetcode.cn/problems/permutation-in-string/
题目描述
给定两个字符串 s1 和 s2,判断 s2 是否包含 s1 的任意一种排列,若存在返回 true,反之返回 false。
等价判定条件:s1 的某个排列能够作为 s2 的一段连续子串出现。
输入输出示例
示例1 输入:s1 = "ab",s2 = "eidbaooo" 输出:true 解释:s2 的子串 "ba" 是 s1 的排列形式之一
示例2 输入:s1 = "ab",s2 = "eidboaoo" 输出:false
核心考点
定长滑动窗口、字符频次统计比对;窗口长度固定为 s1 长度,通过对比窗口与目标字符串的字母频次,判断是否为排列子串
代码实现
c
#define CHAR_TOTAL 26
// 比对两个长度26的频次数组是否完全相等
bool countEqual(int cntA[], int cntB[])
{
for (int idx = 0; idx < CHAR_TOTAL; idx++) {
if (cntA[idx] != cntB[idx]) {
return false;
}
}
return true;
}
bool checkInclusion(char *s1, char *s2)
{
int targetCnt[CHAR_TOTAL] = {0};
int windowCnt[CHAR_TOTAL] = {0};
int k = strlen(s1);
int s2Len = strlen(s2);
// s2更短直接不可能包含s1排列子串
if (k > s2Len) {
return false;
}
// 统计s1目标频次
for (int i = 0; i < k; i++) {
int cIdx = s1[i] - 'a';
targetCnt[cIdx]++;
}
for (int i = 0; i < s2Len; i++) {
int cIdx = s2[i] - 'a';
windowCnt[cIdx]++;
int left = i - k + 1;
if (left < 0) {
continue;
}
// 当前窗口长度达标,比对频次
if (countEqual(windowCnt, targetCnt)) {
return true;
}
// 滑出左边界字符,窗口右移
int leftCIdx = s2[left] - 'a';
windowCnt[leftCIdx]--;
}
return false;
}
438 找到字符串中所有字母异位词
题目链接:https://leetcode.cn/problems/find-all-anagrams-in-a-string/
题目描述
给定两个字符串 s 和 p,找出 s 中全部属于 p 字母异位词的连续子串,返回所有对应起始下标,结果顺序不作要求。字母异位词指字符种类、数量完全一致,排列顺序不同的字符串。
输入输出示例
示例1 输入:s = "cbaebabacd", p = "abc" 输出:0,6 解释:下标0子串cba、下标6子串bac都是abc的异位词
示例2 输入:s = "abab", p = "ab" 输出:0,1,2 解释:三处连续长度为2的子串均为目标异位词
解题思路拆解
-
统计模式串
p的字符频次,作为匹配标准; -
以
p的长度为固定窗口,在字符串s上滑动; -
每次窗口满长后比对频次,完全一致则记录当前左边界下标;
-
窗口右移,剔除左侧出窗字符频次,继续遍历,收集所有合法异位词下标。
核心考点
定长滑动窗口 + 26字母频次比对、多答案收集模板,是567题的拓展升级版
代码实现
c
#define NUM_26 26
bool countEqual(int targetCnt[], int windowCnt[]) {
for (int idx = 0; idx < NUM_26; idx++) {
if (targetCnt[idx] != windowCnt[idx]) {
return false;
}
}
return true;
}
int *findAnagrams(char *s, char *p, int *returnSize) {
int sLen = strlen(s);
int pLen = strlen(p);
*returnSize = 0;
if (pLen > sLen) {
return NULL;
}
int targetCnt[NUM_26] = {0};
int windowCnt[NUM_26] = {0};
for (int i = 0; i < pLen; i++) {
int c = p[i] - 'a';
targetCnt[c]++;
}
int *resArr = (int *)malloc(sizeof(int) * sLen);
int k = pLen;
for (int i = 0; i < sLen; i++) {
int c = s[i] - 'a';
windowCnt[c]++;
int left = i - k + 1;
if (left < 0) {
continue;
}
if (countEqual(targetCnt, windowCnt)) {
resArr[(*returnSize)++] = left;
}
int leftC = s[left] - 'a';
windowCnt[leftC]--;
}
return resArr;
}
187 重复的DNA序列
题目链接:https://leetcode.cn/problems/repeated-dna-sequences/
题目描述
DNA 序列由 A、C、G、T 四种字符组成,给定一个字符串 s,返回所有长度为 10 且出现次数大于等于 2 的子串,返回结果无需有序。
输入输出示例
示例1 输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT" 输出:"AAAAACCCCC","CCCCCAAAAA"
示例2 输入:s = "AAAAAAAAAAAAA" 输出:"AAAAAAAAAA"
解题思路拆解
-
固定窗口长度为 10,遍历截取所有连续子串;
-
借助 uthash 哈希表统计每个子串出现频次;
-
仅收录首次达到两次的子串,避免重复答案;
-
遍历结束释放哈希内存,避免内存泄漏。
核心考点
定长滑动窗口截取子串、uthash 字符串哈希频次统计、重复数据去重收集
代码实现
c
#define NUM_10 10
#define NUM_11 (NUM_10 + 1)
typedef struct {
char key_val[NUM_11];
int cnt;
UT_hash_handle hh;
} Node;
Node *hash_table = NULL;
char **findRepeatedDnaSequences(char *s, int *returnSize)
{
*returnSize = 0;
int sLen = strlen(s);
if (sLen < NUM_10) {
return NULL;
}
char **resArr = (char **)malloc(sizeof(char *) * (sLen - NUM_10 + 1));
char sub[NUM_11];
for (int i = 0; i < sLen; i++) {
int left = i - NUM_10 + 1;
if (left < 0) {
continue;
}
strncpy(sub, s + left, NUM_10);
sub[NUM_10] = '\0';
Node *node = NULL;
HASH_FIND(hh, hash_table, sub, NUM_11, node);
if (node != NULL) {
node->cnt += 1;
// 仅首次重复时收录答案
if (node->cnt == 2) {
resArr[*returnSize] = (char *)malloc(NUM_11);
strcpy(resArr[*returnSize], sub);
(*returnSize)++;
}
} else {
Node *newNode = (Node*)malloc(sizeof(Node));
strcpy(newNode->key_val, sub);
newNode->cnt = 1;
HASH_ADD(hh, hash_table, key_val, NUM_11, newNode);
}
}
// 释放哈希表内存
Node *cur, *tmp;
HASH_ITER(hh, hash_table, cur, tmp) {
HASH_DEL(hash_table, cur);
free(cur);
}
return resArr;
}