一、题目描述(LeetCode 748. 最短补全词)
给定字符串 licensePlate
(含大小写字母、数字、空格等)和单词数组 words
,需从 words
中找到长度最短的 “补全词”,满足以下条件:
1. 包含 licensePlate
中所有字母(忽略非字母字符,不区分大小写,如 'P'
与 'p'
视为同一字母);
2. 每个字母的出现次数不低于 licensePlate
中对应字母的次数(如 licensePlate
含 2 个 's'
,补全词中 's'
至少 2 个);
3. 若存在多个同长度最短补全词,返回数组中最早出现的那种。
示例
输入:licensePlate = "1s3 PSt"
,words = ["step", "steps", "stripe", "stepple"]
预处理:licensePlate
过滤非字母、转小写后为 "spt"
(含 1 个 's'
、1 个 'p'
、1 个 't'
);
筛选:"step"
包含所有目标字母且长度 4("steps"
长 5、"stripe"
长 6、"stepple"
长 6);
输出:"step"
二、解题思路
核心逻辑为 “先明确需求,再筛选匹配”,拆解为 4 个聚焦步骤:
步骤 1:预处理 licensePlate
过滤非字母字符,将所有字母转为小写,生成仅含目标字母的 “需求字符串”,明确需匹配的字母种类。
步骤 2:统计字母需求频次
利用英文字母仅 26 个的特性,用大小为 26 的数组统计频次:
1. 初始化数组为 0,代表初始需求次数为 0;
2. 遍历 “需求字符串”,对每个字符 c
计算索引 idx = c - 'a'
,将数组对应位置 count[idx]
加 1;
3. 最终数组直观体现需求(如 "spt"
对应 count[18]=1
、count[15]=1
、count[19]=1
,其余为 0)。
步骤 3:筛选最短补全词
遍历 words
数组,对每个单词执行以下操作:
1. 用 26 位数组统计当前单词的字母频次;
2. 验证补全词:若单词各字母频次均≥需求频次,即为补全词;
3. 筛选最短:若当前补全词更短,或为首次找到的补全词,更新结果。
步骤 4:返回结果
遍历结束后返回最短补全词,因按数组顺序遍历,同长补全词会优先保留最早出现的。
三、完整代码完成
基于上述思路,C 语言实现如下,含详细注释:
// 全局变量:存储当前最短补全词(跨函数传递结果)
char* result = NULL;
//统计字符串中26个小写字母的频次
void countLetterFreq(char* str, int freq[26]) {
memset(freq, 0, sizeof(int) * 26); // 初始化数组为0
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] >= 'a' && str[i] <= 'z') {
int idx = str[i] - 'a';
freq[idx]++;
}
}
}
//验证单词是否为补全词,更新最短结果
void checkAndUpdate(char* word, char* license) {
int wordFreq[26] = {0};
int licenseFreq[26] = {0};
countLetterFreq(word, wordFreq);
countLetterFreq(license, licenseFreq);
// 验证补全词
int isCompleting = 1;
for (int i = 0; i < 26; i++) {
if (wordFreq[i] < licenseFreq[i]) {
isCompleting = 0;
break;
}
}
// 更新最短结果
if (isCompleting) {
int wordLen = strlen(word);
if (result == NULL || wordLen < strlen(result)) {
result = word;
}
}
}
//主函数:返回最短补全词
char* shortestCompletingWord(char* licensePlate, char** words, int wordsSize) {
// 预处理licensePlate
int plateLen = strlen(licensePlate);
char* license = (char*)malloc(sizeof(char) * (plateLen + 1));
int idx = 0;
for (int i = 0; i < plateLen; i++) {
if (isalpha(licensePlate[i])) {
license[idx++] = tolower(licensePlate[i]);
}
}
license[idx] = '\0'; // 手动加结束符
// 筛选补全词
result = NULL;
for (int i = 0; i < wordsSize; i++) {
checkAndUpdate(words[i], license);
}
free(license);
return result;
}
四、注意事项
C 语言字符串运行细节决定成败,以下 5 个坑点必须规避:
1. 字符串结束符 '\0'
不可漏
预处理 license
后必须加 license[idx] = '\0'
。C 语言字符串以 '\0'
为结束标志,缺少则 strlen
会读取垃圾数据,导致频次统计错误或程序崩溃。
2. 禁止 strlen(NULL)
,做好空指针防护
result
初始为 NULL
,判断时需先检查 result == NULL
,再调用 strlen(result)
。直接计算 strlen(NULL)
会触发空指针访问,程序必然崩溃。
3. 数组不可用 =
直接赋值
C 语言数组名是 “地址常量”,wordFreq = licenseFreq
会编译报错。需通过函数参数传递数组指针,或用 memcpy
复制数组。
4. 动态内存 “申请 - 释放” 需配对
malloc
分配的 license
必须用 free(license)
释放,否则会导致内存泄漏。free
仅能释放动态内存,不可释放静态数组或字符串常量。
5. 全局变量需每次重置
多次调用 shortestCompletingWord
前,需将 result
设为 NULL
,避免上一次结果干扰当前计算。
六、相似题目推荐
为巩固 “频次统计” 与 C 语言能力,推荐 3 道梯度题目:
1. LeetCode 242. 有效的字母异位词(基础)
判断两字符串是否由相同字母组成,直接复用 26 位数组统计逻辑,对比频次数组即可,适合巩固基础。
2. LeetCode 383. 赎金信(进阶)
判断 ransomNote
能否由 magazine
构成,无需遍历单词数组,仅对比两字符串频次,练习频次对比的简化实现。
3. LeetCode 49. 字母异位词分组(综合)
将异位词分组,需结合频次统计与哈希表,将频次数组转化为 “特征键”(如 "a1b2c0..."
),提升综合应用能力。