LeetCode 2374 Count Vowel Strings in Ranges 详解:基于 leetcode 多语言题解的前缀和与位掩码优化
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
本篇技术指南围绕 LeetCode 2374「统计范围内的元音字符串(Count Vowel Strings in Ranges)」展开,完整继承 articles/count-vowel-strings-in-ranges.md 中的三套解法(暴力枚举、前缀和 + 哈希集合、前缀和 + 位掩码)及常见陷阱,并结合 leetcode 仓库的多语言题解组织方式做补充说明。读完后,你将掌握「区间计数」类问题的前缀和建模方法、位掩码做成员判断的技巧,以及 10 种语言下的可复制实现代码。
1. 问题背景与前置知识
1.1 问题定义
给定一个由小写英文字母组成的单词数组words(长度为 n)和 m 个查询queries,每个查询为一个下标对[l, r]。若一个单词的首字符与尾字符都是元音字母(a、e、i、o、u),则称其为「元音字符串(vowel string)」。对每个查询,需要统计words[l .. r]闭区间内元音字符串的个数,并返回长度等于 m 的结果数组。
判断条件有两个关键点:
- 必须同时满足首字符、尾字符为元音,缺一不可;
- 单词本身长度可能为 1,此时首尾是同一个字符,需同时命中元音集合才算有效。
1.2 前置知识(Prerequisites)
原文档列出了三道前置技术点,它们是理解后文三种解法的基石:
| 技术点 | 作用 | 对应解法 |
|---|---|---|
| Prefix Sum(前缀和) | 预计算累积计数,将任意区间查询降到 O(1) | 解法二、三 |
| Hash Set(哈希集合) | 用集合实现 O(1) 的字符元音判定 | 解法一、二 |
| Bit Manipulation(位运算,可选) | 用位掩码代替哈希集合做成员判断 | 解法三 |
2. 解法一:暴力枚举 Brute Force
2.1 直觉
最直接的思路是:对每个查询(start, end),直接遍历该范围内的每个单词,逐个检查首尾字符是否都是元音并累加计数。查询之间互不共享任何中间结果,因此总工作量与「单词总数 × 查询数」同阶。
2.2 算法步骤
- 创建一个元音集合
vowels = {'a','e','i','o','u'},用于 O(1) 查询; - 初始化空的结果列表;
- 对每个查询
(start, end):- 初始化计数器
cnt = 0; - 遍历下标
start到end的每个单词:- 检查首字符与尾字符是否都在元音集合中;
- 若是,
cnt += 1;
- 将
cnt追加到结果列表;
- 初始化计数器
- 返回结果列表。
2.3 参考实现(10 种语言)
Python
class Solution: def vowelStrings(self, words: List[str], queries: List[List[int]]) -> List[int]: vowels = set("aeiou") res = [] for start, end in queries: cnt = 0 for i in range(start, end + 1): if words[i][0] in vowels and words[i][-1] in vowels: cnt += 1 res.append(cnt) return resJava
public class Solution { public int[] vowelStrings(String[] words, int[][] queries) { Set<Character> vowels = Set.of('a', 'e', 'i', 'o', 'u'); int[] res = new int[queries.length]; for (int k = 0; k < queries.length; k++) { int start = queries[k][0], end = queries[k][1], count = 0; for (int i = start; i <= end; i++) { String word = words[i]; if (vowels.contains(word.charAt(0)) && vowels.contains(word.charAt(word.length() - 1))) { count++; } } res[k] = count; } return res; } }C++
class Solution { public: vector<int> vowelStrings(vector<string>& words, vector<vector<int>>& queries) { unordered_set<char> vowels = {'a', 'e', 'i', 'o', 'u'}; vector<int> res; for (auto& q : queries) { int start = q[0], end = q[1], count = 0; for (int i = start; i <= end; i++) { if (vowels.count(words[i][0]) && vowels.count(words[i].back())) { count++; } } res.push_back(count); } return res; } };JavaScript
class Solution { /** * @param {string[]} words * @param {number[][]} queries * @return {number[]} */ vowelStrings(words, queries) { const vowels = new Set(['a', 'e', 'i', 'o', 'u']); const res = []; for (let [start, end] of queries) { let count = 0; for (let i = start; i <= end; i++) { const word = words[i]; if (vowels.has(word[0]) && vowels.has(word[word.length - 1])) { count++; } } res.push(count); } return res; } }C#
public class Solution { public int[] VowelStrings(string[] words, int[][] queries) { HashSet<char> vowels = new HashSet<char> { 'a', 'e', 'i', 'o', 'u' }; int[] res = new int[queries.Length]; for (int k = 0; k < queries.Length; k++) { int start = queries[k][0], end = queries[k][1], count = 0; for (int i = start; i <= end; i++) { string word = words[i]; if (vowels.Contains(word[0]) && vowels.Contains(word[word.Length - 1])) { count++; } } res[k] = count; } return res; } }Go
func vowelStrings(words []string, queries [][]int) []int { vowels := map[byte]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true} res := make([]int, len(queries)) for k, q := range queries { start, end, count := q[0], q[1], 0 for i := start; i <= end; i++ { word := words[i] if vowels[word[0]] && vowels[word[len(word)-1]] { count++ } } res[k] = count } return res }Kotlin
class Solution { fun vowelStrings(words: Array<String>, queries: Array<IntArray>): IntArray { val vowels = setOf('a', 'e', 'i', 'o', 'u') val res = IntArray(queries.size) for (k in queries.indices) { val (start, end) = queries[k] var count = 0 for (i in start..end) { val word = words[i] if (word.first() in vowels && word.last() in vowels) { count++ } } res[k] = count } return res } }Swift
class Solution { func vowelStrings(_ words: [String], _ queries: [[Int]]) -> [Int] { let vowels: Set<Character> = ["a", "e", "i", "o", "u"] var res = [Int]() for q in queries { let start = q[0], end = q[1] var count = 0 for i in start...end { let word = words[i] if vowels.contains(word.first!) && vowels.contains(word.last!) { count += 1 } } res.append(count) } return res } }Rust
impl Solution { pub fn vowel_strings(words: Vec<String>, queries: Vec<Vec<i32>>) -> Vec<i32> { let vowels: HashSet<u8> = [b'a', b'e', b'i', b'o', b'u'].into(); let mut res = Vec::with_capacity(queries.len()); for q in &queries { let (start, end) = (q[0] as usize, q[1] as usize); let mut count = 0; for i in start..=end { let w = words[i].as_bytes(); if vowels.contains(&w[0]) && vowels.contains(&w[w.len() - 1]) { count += 1; } } res.push(count); } res } }TypeScript
class Solution { /** * @param {string[]} words * @param {number[][]} queries * @return {number[]} */ vowelStrings(words: string[], queries: number[][]): number[] { const vowels = new Set(['a', 'e', 'i', 'o', 'u']); const res: number[] = []; for (const [start, end] of queries) { let count = 0; for (let i = start; i <= end; i++) { const word = words[i]; if (vowels.has(word[0]) && vowels.has(word[word.length - 1])) { count++; } } res.push(count); } return res; } }2.4 复杂度
- 时间复杂度:
O(n * m)(n 为单词数量,m 为查询数量;最坏情况下每个查询都要遍历全部单词); - 空间复杂度:额外
O(1),输出列表占O(m)。
当 n 与 m 都接近 10^4 量级时,O(n * m)可达 10^8 次字符检查,已逼近超时风险,因此需要引入前缀和。
3. 解法二:前缀和 + 哈希集合 Prefix Sum + Hash Set
3.1 直觉
暴力法的核心问题是重复劳动:多个查询之间存在大量重叠区间,每次都重新扫描。由于「元音字符串判定」对每个单词只依赖其自身、且与查询无关,可以一次性预计算前缀计数数组prefix[i]——表示从下标0到i-1(共 i 个单词)中元音字符串的个数。这样任意区间查询(l, r)只需一次减法即可 O(1) 回答:
answer(l, r) = prefix[r + 1] - prefix[l]这就是标准的「静态区间求和」前缀和模型(与 LeetCode 303 Range Sum Query - Immutable 同型,仓库中也有对应多语言题解,例如 python/0303-range-sum-query-immutable.py)。
3.2 算法步骤
- 创建元音集合用于 O(1) 查询;
- 建立长度为
n + 1的前缀数组prefixCnt,初始全 0(prefixCnt[0] = 0作为哨兵,方便闭区间查询); - 对下标
i处的单词:prefixCnt[i + 1] = prefixCnt[i];- 若该单词首尾均为元音,则
prefixCnt[i + 1] += 1;
- 对每个查询
(l, r),答案为prefixCnt[r + 1] - prefixCnt[l]; - 返回结果数组。
注意前缀数组比words多一位(n + 1),右端点必须取r + 1,这是后续「常见陷阱」中第一个错误点的根源。
3.3 参考实现(10 种语言)
Python
class Solution: def vowelStrings(self, words: List[str], queries: List[List[int]]) -> List[int]: vowel_set = set("aeiou") prefix_cnt = [0] * (len(words) + 1) prev = 0 for i, w in enumerate(words): if w[0] in vowel_set and w[-1] in vowel_set: prev += 1 prefix_cnt[i + 1] = prev res = [0] * len(queries) for i, q in enumerate(queries): l, r = q res[i] = prefix_cnt[r + 1] - prefix_cnt[l] return resJava
public class Solution { public int[] vowelStrings(String[] words, int[][] queries) { Set<Character> vowels = Set.of('a', 'e', 'i', 'o', 'u'); int n = words.length; int[] prefixCnt = new int[n + 1]; for (int i = 0; i < n; i++) { String w = words[i]; prefixCnt[i + 1] = prefixCnt[i]; if (vowels.contains(w.charAt(0)) && vowels.contains(w.charAt(w.length() - 1))) { prefixCnt[i + 1]++; } } int[] res = new int[queries.length]; for (int i = 0; i < queries.length; i++) { int l = queries[i][0], r = queries[i][1]; res[i] = prefixCnt[r + 1] - prefixCnt[l]; } return res; } }C++
class Solution { public: vector<int> vowelStrings(vector<string>& words, vector<vector<int>>& queries) { unordered_set<char> vowels = {'a', 'e', 'i', 'o', 'u'}; int n = words.size(); vector<int> prefixCnt(n + 1, 0); for (int i = 0; i < n; i++) { prefixCnt[i + 1] = prefixCnt[i]; if (vowels.count(words[i][0]) && vowels.count(words[i].back())) { prefixCnt[i + 1]++; } } vector<int> res; for (auto& q : queries) { int l = q[0], r = q[1]; res.push_back(prefixCnt[r + 1] - prefixCnt[l]); } return res; } };JavaScript
class Solution { /** * @param {string[]} words * @param {number[][]} queries * @return {number[]} */ vowelStrings(words, queries) { const vowels = new Set(['a', 'e', 'i', 'o', 'u']); const n = words.length; const prefixCnt = new Array(n + 1).fill(0); for (let i = 0; i < n; i++) { prefixCnt[i + 1] = prefixCnt[i]; const w = words[i]; if (vowels.has(w[0]) && vowels.has(w[w.length - 1])) { prefixCnt[i + 1]++; } } const res = new Array(queries.length); for (let i = 0; i < queries.length; i++) { const [l, r] = queries[i]; res[i] = prefixCnt[r + 1] - prefixCnt[l]; } return res; } }C#
public class Solution { public int[] VowelStrings(string[] words, int[][] queries) { HashSet<char> vowels = new HashSet<char> { 'a', 'e', 'i', 'o', 'u' }; int n = words.Length; int[] prefixCnt = new int[n + 1]; for (int i = 0; i < n; i++) { string w = words[i]; prefixCnt[i + 1] = prefixCnt[i]; if (vowels.Contains(w[0]) && vowels.Contains(w[w.Length - 1])) { prefixCnt[i + 1]++; } } int[] res = new int[queries.Length]; for (int i = 0; i < queries.Length; i++) { int l = queries[i][0], r = queries[i][1]; res[i] = prefixCnt[r + 1] - prefixCnt[l]; } return res; } }Go
func vowelStrings(words []string, queries [][]int) []int { vowels := map[byte]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true} n := len(words) prefixCnt := make([]int, n+1) for i := 0; i < n; i++ { w := words[i] prefixCnt[i+1] = prefixCnt[i] if vowels[w[0]] && vowels[w[len(w)-1]] { prefixCnt[i+1]++ } } res := make([]int, len(queries)) for i, q := range queries { l, r := q[0], q[1] res[i] = prefixCnt[r+1] - prefixCnt[l] } return res }Kotlin
class Solution { fun vowelStrings(words: Array<String>, queries: Array<IntArray>): IntArray { val vowels = setOf('a', 'e', 'i', 'o', 'u') val n = words.size val prefixCnt = IntArray(n + 1) for (i in 0 until n) { val w = words[i] prefixCnt[i + 1] = prefixCnt[i] if (w.first() in vowels && w.last() in vowels) { prefixCnt[i + 1]++ } } val res = IntArray(queries.size) for (i in queries.indices) { val (l, r) = queries[i] res[i] = prefixCnt[r + 1] - prefixCnt[l] } return res } }Swift
class Solution { func vowelStrings(_ words: [String], _ queries: [[Int]]) -> [Int] { let vowels: Set<Character> = ["a", "e", "i", "o", "u"] let n = words.count var prefixCnt = Int for i in 0..<n { let w = words[i] prefixCnt[i + 1] = prefixCnt[i] if vowels.contains(w.first!) && vowels.contains(w.last!) { prefixCnt[i + 1] += 1 } } var res = [Int]() for q in queries { let l = q[0], r = q[1] res.append(prefixCnt[r + 1] - prefixCnt[l]) } return res } }Rust
impl Solution { pub fn vowel_strings(words: Vec<String>, queries: Vec<Vec<i32>>) -> Vec<i32> { let vowels: HashSet<u8> = [b'a', b'e', b'i', b'o', b'u'].into(); let n = words.len(); let mut prefix_cnt = vec![0i32; n + 1]; for i in 0..n { let w = words[i].as_bytes(); prefix_cnt[i + 1] = prefix_cnt[i]; if vowels.contains(&w[0]) && vowels.contains(&w[w.len() - 1]) { prefix_cnt[i + 1] += 1; } } queries .iter() .map(|q| { let (l, r) = (q[0] as usize, q[1] as usize); prefix_cnt[r + 1] - prefix_cnt[l] }) .collect() } }TypeScript
class Solution { /** * @param {string[]} words * @param {number[][]} queries * @return {number[]} */ vowelStrings(words: string[], queries: number[][]): number[] { const vowels = new Set(['a', 'e', 'i', 'o', 'u']); const n = words.length; const prefixCnt: number[] = new Array(n + 1).fill(0); for (let i = 0; i < n; i++) { prefixCnt[i + 1] = prefixCnt[i]; const w = words[i]; if (vowels.has(w[0]) && vowels.has(w[w.length - 1])) { prefixCnt[i + 1]++; } } const res: number[] = new Array(queries.length); for (let i = 0; i < queries.length; i++) { const [l, r] = queries[i]; res[i] = prefixCnt[r + 1] - prefixCnt[l]; } return res; } }3.4 复杂度
- 时间复杂度:
O(n + m)——一次线性扫描建前缀,每个查询 O(1); - 空间复杂度:额外
O(n)(前缀数组),输出O(m)。
3.5 实现要点
从各语言实现可以看到几个值得注意的工程细节:
- 哨兵位:
prefixCnt[0] = 0使左端点直接以l下标相减即可,无需特殊处理l = 0的情况; - Python 版用局部变量
prev逐步累加后赋给prefix_cnt[i + 1],避免反复读写数组下标,语义与prefixCnt[i+1] = prefixCnt[i] + isVowel等价; - Java/C++ 等采用「先拷贝、再增量」的两步写法(
prefix[i+1] = prefix[i],命中后++),与 Rust 版iter().map().collect()的函数式写法在逻辑上完全一致。
4. 解法三:前缀和 + 位掩码 Prefix Sum + Bitmask
4.1 直觉
哈希集合虽然也是 O(1) 判元音,但引入了堆分配与哈希开销。由于元音只有 5 个、小写字母只有 26 个,可以用一个整数的 26 个二进制位编码「哪些字符是元音」:位i为 1 当且仅当字符'a' + i是元音。判断某字符c是否元音,退化为一次按位与:
isVowel(c) <=> ((1 << (c - 'a')) & vowels) != 0原文档给出的掩码构造方式为循环左移取并:
for c in "aeiou": vowels |= 1 << (ord(c) - ord('a'))对应各元音占用的位:a→bit 0、e→bit 4、i→bit 8、o→bit 14、u→bit 20。可推断该掩码是一个常量:2^0 + 2^4 + 2^8 + 2^14 + 2^20 = 1 + 16 + 256 + 16384 + 1048576 = 1065233,因此熟练后甚至可以直接写const VOYEL_MASK = 1065233(建议保留循环构造法,可读性更好且不易写错)。
4.2 算法步骤
- 通过循环置位构造元音位掩码(
a、e、i、o、u); - 建立长度为
n + 1的前缀数组prefix; - 对每个单词:
- 检查首字符对应位是否在掩码中置位;
- 检查尾字符对应位是否在掩码中置位;
- 两者均满足则前缀计数加 1;
- 对每个查询
(l, r),答案为prefix[r + 1] - prefix[l]; - 返回结果数组。
4.3 参考实现(10 种语言)
Python
class Solution: def vowelStrings(self, words: List[str], queries: List[List[int]]) -> List[int]: vowels = sum(1 << (ord(c) - ord('a')) for c in "aeiou") prefix = [0] for w in words: prefix.append(prefix[-1]) if (1 << (ord(w[0]) - ord('a'))) & vowels and (1 << (ord(w[-1]) - ord('a'))) & vowels: prefix[-1] += 1 return [prefix[r + 1] - prefix[l] for l, r in queries]Java
public class Solution { public int[] vowelStrings(String[] words, int[][] queries) { int vowels = 0; for (char c : "aeiou".toCharArray()) { vowels |= 1 << (c - 'a'); } int[] prefix = new int[words.length + 1]; for (int i = 0; i < words.length; i++) { int f = words[i].charAt(0) - 'a'; int l = words[i].charAt(words[i].length() - 1) - 'a'; int isVowel = ((1 << f) & vowels) != 0 && ((1 << l) & vowels) != 0 ? 1 : 0; prefix[i + 1] = prefix[i] + isVowel; } int[] res = new int[queries.length]; for (int i = 0; i < queries.length; i++) { int l = queries[i][0], r = queries[i][1]; res[i] = prefix[r + 1] - prefix[l]; } return res; } }C++
class Solution { public: vector<int> vowelStrings(vector<string>& words, vector<vector<int>>& queries) { int vowels = 0; for (char c : string("aeiou")) { vowels |= (1 << (c - 'a')); } int n = words.size(); vector<int> prefix(n + 1); for (int i = 0; i < n; i++) { int f = words[i][0] - 'a'; int l = words[i].back() - 'a'; int isVowel = ((1 << f) & vowels) && ((1 << l) & vowels); prefix[i + 1] = prefix[i] + isVowel; } vector<int> res; for (auto& q : queries) { int l = q[0], r = q[1]; res.push_back(prefix[r + 1] - prefix[l]); } return res; } };JavaScript
class Solution { /** * @param {string[]} words * @param {number[][]} queries * @return {number[]} */ vowelStrings(words, queries) { let vowels = 0; for (let c of 'aeiou') { vowels |= 1 << (c.charCodeAt(0) - 97); } const prefix = [0]; for (let w of words) { const f = w.charCodeAt(0) - 97; const l = w.charCodeAt(w.length - 1) - 97; const isVowel = (1 << f) & vowels && (1 << l) & vowels ? 1 : 0; prefix.push(prefix[prefix.length - 1] + isVowel); } return queries.map(([l, r]) => prefix[r + 1] - prefix[l]); } }C#
public class Solution { public int[] VowelStrings(string[] words, int[][] queries) { int vowels = 0; foreach (char c in "aeiou") { vowels |= 1 << (c - 'a'); } int[] prefix = new int[words.Length + 1]; for (int i = 0; i < words.Length; i++) { int f = words[i][0] - 'a'; int l = words[i][words[i].Length - 1] - 'a'; int isVowel = ((1 << f) & vowels) != 0 && ((1 << l) & vowels) != 0 ? 1 : 0; prefix[i + 1] = prefix[i] + isVowel; } int[] res = new int[queries.Length]; for (int i = 0; i < queries.Length; i++) { int l = queries[i][0], r = queries[i][1]; res[i] = prefix[r + 1] - prefix[l]; } return res; } }Go
func vowelStrings(words []string, queries [][]int) []int { vowels := 0 for _, c := range "aeiou" { vowels |= 1 << (c - 'a') } n := len(words) prefix := make([]int, n+1) for i := 0; i < n; i++ { f := int(words[i][0] - 'a') l := int(words[i][len(words[i])-1] - 'a') isVowel := 0 if (1<<f)&vowels != 0 && (1<<l)&vowels != 0 { isVowel = 1 } prefix[i+1] = prefix[i] + isVowel } res := make([]int, len(queries)) for i, q := range queries { l, r := q[0], q[1] res[i] = prefix[r+1] - prefix[l] } return res }Kotlin
class Solution { fun vowelStrings(words: Array<String>, queries: Array<IntArray>): IntArray { var vowels = 0 for (c in "aeiou") { vowels = vowels or (1 shl (c - 'a')) } val prefix = IntArray(words.size + 1) for (i in words.indices) { val f = words[i][0] - 'a' val l = words[i].last() - 'a' val isVowel = if ((1 shl f) and vowels != 0 && (1 shl l) and vowels != 0) 1 else 0 prefix[i + 1] = prefix[i] + isVowel } return IntArray(queries.size) { i -> val (lo, hi) = queries[i] prefix[hi + 1] - prefix[lo] } } }Swift
class Solution { func vowelStrings(_ words: [String], _ queries: [[Int]]) -> [Int] { var vowels = 0 for c in "aeiou" { vowels |= 1 << (Int(c.asciiValue!) - 97) } var prefix = [0] for w in words { let f = Int(w.first!.asciiValue!) - 97 let l = Int(w.last!.asciiValue!) - 97 let isVowel = ((1 << f) & vowels != 0 && (1 << l) & vowels != 0) ? 1 : 0 prefix.append(prefix.last! + isVowel) } return queries.map { q in let l = q[0], r = q[1] return prefix[r + 1] - prefix[l] } } }Rust
impl Solution { pub fn vowel_strings(words: Vec<String>, queries: Vec<Vec<i32>>) -> Vec<i32> { let mut vowels = 0u32; for c in "aeiou".bytes() { vowels |= 1 << (c - b'a'); } let n = words.len(); let mut prefix = vec![0i32; n + 1]; for i in 0..n { let w = words[i].as_bytes(); let f = w[0] - b'a'; let l = w[w.len() - 1] - b'a'; let is_vowel = if (1 << f) & vowels != 0 && (1 << l) & vowels != 0 { 1 } else { 0 }; prefix[i + 1] = prefix[i] + is_vowel; } queries .iter() .map(|q| { let (l, r) = (q[0] as usize, q[1] as usize); prefix[r + 1] - prefix[l] }) .collect() } }TypeScript
class Solution { /** * @param {string[]} words * @param {number[][]} queries * @return {number[]} */ vowelStrings(words: string[], queries: number[][]): number[] { let vowels = 0; for (const c of 'aeiou') { vowels |= 1 << (c.charCodeAt(0) - 97); } const prefix: number[] = [0]; for (const w of words) { const f = w.charCodeAt(0) - 97; const l = w.charCodeAt(w.length - 1) - 97; const isVowel = (1 << f) & vowels && (1 << l) & vowels ? 1 : 0; prefix.push(prefix[prefix.length - 1] + isVowel); } return queries.map(([l, r]) => prefix[r + 1] - prefix[l]); } }4.4 复杂度
- 时间复杂度:
O(n + m); - 空间复杂度:额外
O(n),输出O(m)。
4.5 语言层面的位运算细节
从 10 份实现中可以归纳出移植位掩码方案时的几个关键点:
- 字符到位移量的转换:C/Java/C++ 直接
c - 'a';JS/TS 用charCodeAt(0) - 97;Swift 用asciiValue! - 97;Rust 用as_bytes()后c - b'a';Kotlin 用1 shl x表示左移。本质都是把字符映射到0..25; - 移位量上限:小写字母最大位移 25(
'z' - 'a'),任何 32 位整型都足够容纳,无溢出风险; - 按位与的返回值差异:C++ 中
((1 << f) & vowels)直接得到 0/非 0 整数可参与布尔表达式(如int isVowel = ((1 << f) & vowels) && ((1 << l) & vowels););Java、C# 显式比较!= 0;Go、Rust 也显式写!= 0。跨语言对照阅读时注意这一点,避免漏写比较。
5. 常见陷阱 Common Pitfalls
原文档总结了三个高频错误,配合反例代码说明:
5.1 前缀和查询的 Off-by-One
最经典的错误是把查询写成prefix[r] - prefix[l],漏掉了右端点的+1。前缀数组是「以 1 为基的偏移式」(prefix[i]表示[0, i-1]的累计),因此闭区间[l, r]的右端必须取r + 1:
# Incorrect - excludes the element at index r res[i] = prefix[r] - prefix[l] # Correct - includes the element at index r res[i] = prefix[r + 1] - prefix[l]5.2 只检查首字符或尾字符
元音字符串必须首尾同时为元音。只写一个条件会把大量单词误判进来:
# Incorrect - only checks starting character if words[i][0] in vowels: cnt += 1 # Correct - checks both first and last characters if words[i][0] in vowels and words[i][-1] in vowels: cnt += 15.3 元音集合/掩码漏项
构造元音集合或掩码时漏掉 5 个元音之一(a、e、i、o、u)会导致系统性漏计,手工拼掩码常量时尤其容易出错:
# Incorrect - missing 'u' vowels = set("aeio") # Correct - all five vowels included vowels = set("aeiou")这也是前文建议保留「循环置位构造掩码」而非硬编码1065233的原因——构造过程本身就是一种自检。
6. 三种解法对比与仓库组织说明
6.1 复杂度对比
| 解法 | 时间复杂度 | 额外空间 | 适用场景 |
|---|---|---|---|
| 暴力枚举 | O(n * m) | O(1) | 教学理解、n·m 很小的场景 |
| 前缀和 + 哈希集合 | O(n + m) | O(n) | 通用最优,可读性最好 |
| 前缀和 + 位掩码 | O(n + m) | O(n) | 追求常数性能、展示位技巧 |
其中 n 为单词数量,m 为查询数量。三种解法共享同一个正确性骨架(首尾双元音判定 + 区间闭端语义),差异仅在「成员判定数据结构」(无 / 哈希集合 / 位掩码)与「区间统计方式」(逐次扫描 / 前缀减法)。
6.2 在 leetcode 仓库中的位置
- 本文所有算法、代码与陷阱章节均完整继承自 articles/count-vowel-strings-in-ranges.md,原文档中每个解法以 Python、Java、C++、JavaScript、C#、Go、Kotlin、Swift、Rust、TypeScript 共 10 种语言并列给出;
- 仓库的题解文章规范见 articles/README.md:每篇文章要求覆盖尽可能多的解法(且至少一种与 NeetCode 视频解法一致)、标注时间与空间复杂度——原文档的结构正是按该规范编写的,这也解释了为何每种语言实现都重复完整呈现;
- 这 10 种语言与 README.md 中列出的 NeetCode 支持语言清单(Python、Java、JavaScript、C++、Go、Swift、C#、TypeScript、Rust、Kotlin 等)相对应;仓库按
python/、java/、cpp/等语言目录存放其他题目的解码文件,文件命名遵循「题号-题目短横线命名」惯例(例如 python/1456-maximum-number-of-vowels-in-a-substring-of-given-length.py)。需要注意的是,本仓库当前各语言目录中并未收录 2374 号题的独立代码文件,因此本文的 30 段实现代码全部以原文档为唯一事实来源,可直接复制到 LeetCode 判题环境运行。
6.3 小结
- 面对「静态数组 + 大量区间计数/求和查询」,第一反应应是前缀和:一次性 O(n) 预处理换取 O(1) 查询;
- 字符类成员判定可用哈希集合(通用)或位掩码(26 个小写字母场景下的零分配优化);
- 移植前务必守住三条正确性底线:右端点
+1、首尾双条件、元音五要素一个不能少。
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考