题目
键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。
给你一个由若干单词组成的字符串 text ,单词间由单个空格组成(不含前导和尾随空格);另有一个字符串 brokenLetters ,由所有已损坏的不同字母键组成,返回你可以使用此键盘完全输入的 text 中单词的数目。
示例 1:
输入:text = "hello world", brokenLetters = "ad" 输出:1 解释:无法输入 "world" ,因为字母键 'd' 已损坏。
示例 2:
输入:text = "leet code", brokenLetters = "lt" 输出:1 解释:无法输入 "leet" ,因为字母键 'l' 和 't' 已损坏。
示例 3:
输入:text = "leet code", brokenLetters = "e" 输出:0 解释:无法输入任何单词,因为字母键 'e' 已损坏。
提示:
1 <= text.length <= 1040 <= brokenLetters.length <= 26text由若干用单个空格分隔的单词组成,且不含任何前导和尾随空格- 每个单词仅由小写英文字母组成
brokenLetters由 互不相同 的小写英文字母组成
核心思想
这道题的核心思想是:先将所有损坏的字母存入一个高效的查询数据结构中,然后遍历文本中的每一个单词,并检查该单词是否包含任何损坏的字母。
我们可以将这个思想拆解成一个清晰的算法流程。
解题步骤
第一步:预处理损坏字母 (Preprocessing the Broken Letters)
为了能够快速判断一个字母是否损坏,我们不能每次都去遍历 brokenLetters 字符串。最高效的方式是先将这些损坏的字母存入一个查询时间复杂度为 O(1) 的数据结构中。
- 最佳选择:
- 哈希集合 (Hash Set):比如 C++ 的
std::unordered_set或 Python 的set。这是最通用的方法。将brokenLetters中的所有字符都添加进去。 - 布尔数组 (Boolean Array):由于题目限定了字符范围是小写英文字母(共26个),我们可以创建一个大小为26的布尔数组,例如
bool is_broken[26]。通过letter - 'a'将字母映射到数组索引0-25。如果is_broken[i]为true,则表示对应的字母是损坏的。这种方法在字符集很小且固定的情况下,通常比哈希集合更快。
- 哈希集合 (Hash Set):比如 C++ 的
完成这一步后,我们就有了一个可以瞬间判断任意字母是否损坏的“查询器”。
第二步:分割字符串以获取所有单词 (Splitting the String)
最直观的方法是将输入的 text 字符串按照空格进行分割,得到一个包含所有单词的列表(或数组)。几乎所有主流编程语言都提供了内置的 split 函数来完成这个任务。
例如,"hello world" 会被分割成 ["hello", "world"]。
第三步:遍历并验证每个单词 (Iterating and Validating Each Word)
现在我们有了一个单词列表和一个高效的“损坏字母查询器”,接下来就是验证过程:
- 初始化一个最终计数器
count = 0。 - 遍历第二步得到的单词列表中的每一个单词。
- 对于每一个单词,我们需要判断它是否“有效”。我们可以设定一个临时的布尔标志位,比如
is_word_valid = true,先假设这个单词是有效的。 - 接着,遍历当前单词中的每一个字符。
- 对于每个字符,使用第一步创建的查询器(哈希集合或布尔数组)检查它是否是损坏的。
- 关键逻辑:
- 如果发现任何一个字符是损坏的,那么这个单词就无效了。我们立刻将
is_word_valid设为false,并且可以提前跳出内层循环(使用break),因为没有必要再检查这个单词剩下的字符了。 - 如果内层循环正常结束(即没有中途
break),说明这个单词的所有字符都不是损坏的,is_word_valid将保持为true。
- 如果发现任何一个字符是损坏的,那么这个单词就无效了。我们立刻将
- 内层循环结束后,检查
is_word_valid的值。如果它为true,则将最终计数器count加一。
第四步:返回结果
遍历完所有单词后,count 的值就是你可以输入的单词总数。
另一种思路:单次遍历(不分割字符串)
你之前提供的两份代码都采用了这种更优化的思路。它避免了 split 操作可能带来的额外内存开销(即创建一个新的单词列表)。
- 思路:直接遍历原始的
text字符串,一次一个字符。 - 状态维护:用一个状态变量(例如
current_word_is_broken)来跟踪当前正在扫描的单词是否已经包含了损坏字母。 - 逻辑:
- 当遇到一个字母时,就去查询它是否损坏。如果损坏,就把状态变量设为
true。 - 当遇到一个空格或到达字符串末尾时,这就标志着一个单词的结束。
- 此时,检查状态变量。如果它仍然是
false,说明刚刚结束的这个单词是有效的,就把最终计数器加一。 - 重置状态:在处理完一个单词后,必须将状态变量重置为
false,以便正确检查下一个单词。
- 当遇到一个字母时,就去查询它是否损坏。如果损坏,就把状态变量设为
这两种思路最终都能得到正确答案。第一种(先分割)在逻辑上更简单直观,而第二种(单次遍历)在性能和内存使用上通常更胜一筹。
具体代码
class Solution {
public:
int canBeTypedWords(string text, string brokenLetters) {
vector<bool> broken(26, 0);
for(const char& letter : brokenLetters)
broken[letter - 'a'] = 1;
int ans = 0;
bool word_broken = false;
text = text + " ";
for(const char& letter : text)
{
if(letter == ' ') // 每一个新单词计算是否可以打,并重新计算
{
if(!word_broken)
ans++;
word_broken = false;
continue; // 避免空格影响下面
}
if(broken[letter - 'a']) // 错误,这个单词不算。
word_broken = true;
}
return ans;
}
};