1935. 可以输入的最大单词数

题目

键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。

给你一个由若干单词组成的字符串 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 <= 104
  • 0 <= brokenLetters.length <= 26
  • text 由若干用单个空格分隔的单词组成,且不含任何前导和尾随空格
  • 每个单词仅由小写英文字母组成
  • brokenLetters 由 互不相同 的小写英文字母组成

核心思想

这道题的核心思想是:先将所有损坏的字母存入一个高效的查询数据结构中,然后遍历文本中的每一个单词,并检查该单词是否包含任何损坏的字母。

我们可以将这个思想拆解成一个清晰的算法流程。

解题步骤

第一步:预处理损坏字母 (Preprocessing the Broken Letters)

为了能够快速判断一个字母是否损坏,我们不能每次都去遍历 brokenLetters 字符串。最高效的方式是先将这些损坏的字母存入一个查询时间复杂度为 O(1) 的数据结构中。

  • 最佳选择
    1. 哈希集合 (Hash Set):比如 C++ 的 std::unordered_set 或 Python 的 set。这是最通用的方法。将 brokenLetters 中的所有字符都添加进去。
    2. 布尔数组 (Boolean Array):由于题目限定了字符范围是小写英文字母(共26个),我们可以创建一个大小为26的布尔数组,例如 bool is_broken[26]。通过 letter - 'a' 将字母映射到数组索引 0-25。如果 is_broken[i] 为 true,则表示对应的字母是损坏的。这种方法在字符集很小且固定的情况下,通常比哈希集合更快。

完成这一步后,我们就有了一个可以瞬间判断任意字母是否损坏的“查询器”。

第二步:分割字符串以获取所有单词 (Splitting the String)

最直观的方法是将输入的 text 字符串按照空格进行分割,得到一个包含所有单词的列表(或数组)。几乎所有主流编程语言都提供了内置的 split 函数来完成这个任务。

例如,"hello world" 会被分割成 ["hello", "world"]

第三步:遍历并验证每个单词 (Iterating and Validating Each Word)

现在我们有了一个单词列表和一个高效的“损坏字母查询器”,接下来就是验证过程:

  1. 初始化一个最终计数器 count = 0
  2. 遍历第二步得到的单词列表中的每一个单词。
  3. 对于每一个单词,我们需要判断它是否“有效”。我们可以设定一个临时的布尔标志位,比如 is_word_valid = true,先假设这个单词是有效的。
  4. 接着,遍历当前单词中的每一个字符。
  5. 对于每个字符,使用第一步创建的查询器(哈希集合或布尔数组)检查它是否是损坏的。
  6. 关键逻辑
    • 如果发现任何一个字符是损坏的,那么这个单词就无效了。我们立刻将 is_word_valid 设为 false,并且可以提前跳出内层循环(使用 break),因为没有必要再检查这个单词剩下的字符了。
    • 如果内层循环正常结束(即没有中途 break),说明这个单词的所有字符都不是损坏的,is_word_valid 将保持为 true
  7. 内层循环结束后,检查 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;
    }
};