目录

题目描述

925. 长按键入

题意分析

你的朋友在键盘上输入 name,但某些按键可能被长按了,导致对应字符在结果里连续重复多次。给出实际打出来的字符串 typed,判断它是否可能由输入 name 时长按产生。

「长按」这个动作的语义要抠准:它只能让某个字符重复更多次,不能凭空插入别的字符,也不能漏掉任何字符,更不能改变顺序。所以 typed 必须能被切成若干连续段,每段字符与 name 的对应字符相同,且每段长度大于等于 name 中该字符的连续次数。

注意长按次数可以是 0 次额外重复——也就是 typedname 完全相同时答案为真。题目问的是「可能」,不要求真的发生过长按。

约束里两串长度都在 1 到 1000 之间,全是小写字母。规模极小,$O(m + n)$ 一遍扫描足够。这题的分数全在边界与终止条件上,不在效率。

边界要盯住三处:typedname 短时必然为假;typed 扫完但 name 还有剩余字符时也为假(比如 name = "alex"typed = "ale");typed 结尾多出一段合法长按(比如 typed = "alexx")时为真。第二处最容易漏,它决定了循环退出后还得再判一次。

解法:双指针扫描

核心思路

一种直觉做法是把两个串都压缩成「字符 + 连续次数」的分组形式,再逐组比较:字符必须相同,且 typed 的次数不小于 name 的次数,组数也必须一致。这个做法完全正确、可读性也好,但要额外开两个 $O(n)$ 的列表来存分组结果。

瓶颈在于我们把「分组」和「比较」拆成了两个阶段。而分组信息其实可以边扫边用——判断某个字符是不是长按产生的,只需要看它与前一个字符是否相同,不必真的统计次数。

于是用双指针:i 指向 name 中待匹配的字符,j 扫描 typed。每一步只有三种情况,且是互斥有序的。

第一,name[i] == typed[j]:这是一次正常匹配,两个指针同时前进。这一支必须优先判断——只要能正常匹配就正常匹配,把「算作长按」留给匹配不上的时候,否则会出现该推进 i 却没推进的错配。

第二,匹配不上,但 typed[j] == typed[j-1]:说明当前字符是前一个字符的重复,即长按产生的多余字符,只推进 ji 原地不动。

第三,两者都不成立:typed[j] 既不是 name 需要的字符,也不是上一个字符的延续,它是凭空多出来的,直接返回假。

不变量:每轮循环开始时,typed 的前 j 个字符恰好可以由 name 的前 i 个字符经过若干次长按生成,且这个 i 是所有可行方案里唯一的取值(贪心匹配不会错过更优解——因为字符相同时立即匹配总不劣,留着不匹配只会让后续更难)。

循环以 j 走完 typed 为终止条件。退出时 typed 全部解释完毕,但 name 未必用完,所以最后必须再判一次 i == name.length()。这一句是全题的收口:它把「typed 太短、没能覆盖 name 的全部字符」这一类错误挡在门外。

解题步骤

  • 初始化 i = 0j = 0:两个指针都从头开始,分别追踪「name 已匹配到哪」和「typed 已解释到哪」。
  • 循环条件用 j < typed.length():以 typed 为主导,因为每个 typed 字符都必须被解释成「正常匹配」或「长按重复」二者之一,一个都不能剩。若改用 i < name.length() 作条件,typed 尾部多出的非法字符(如 name = "a"typed = "ab")就检查不到。
  • 优先分支:i < name.length() && name.charAt(i) == typed.charAt(j)i < name.length() 的越界保护必须写在前面并用短路与,否则 name 用完后 charAt(i) 会直接抛异常。命中则 i++j++
  • 次选分支:j > 0 && typed.charAt(j) == typed.charAt(j - 1)j > 0 保护了 j - 1 不越界;j = 0 时若第一个字符就匹配不上,本来也该判假。命中则只 j++i 不动——长按不消耗 name 的字符。
  • 两分支都不成立返回 false:这个字符无法解释,立刻否定,不必继续。
  • 循环外返回 i == name.length():不能直接 return truetyped 解释完了不等于 name 匹配完了。

name = "alex"typed = "aaleex" 走一遍,正确答案是 true。

初始 i = 0j = 0

第 1 步:name[0] = 'a'typed[0] = 'a' 相同,正常匹配,i = 1j = 1

第 2 步:name[1] = 'l'typed[1] = 'a' 不同;typed[1] = 'a'typed[0] = 'a' 相同,判定为长按,只推进 j = 2。这里 i 保持在 1,意味着字母 l 还等着被匹配。

第 3 步:name[1] = 'l'typed[2] = 'l' 相同,正常匹配,i = 2j = 3

第 4 步:name[2] = 'e'typed[3] = 'e' 相同,i = 3j = 4

第 5 步:name[3] = 'x'typed[4] = 'e' 不同;typed[4] = 'e'typed[3] = 'e' 相同,长按,j = 5

第 6 步:name[3] = 'x'typed[5] = 'x' 相同,i = 4j = 6

j = 6 等于 typed 长度,循环退出。i = 4 等于 name 长度,返回 true。

再看一个假的用例 name = "saeed"typed = "ssaaedd"。前面几步依次是:s 匹配(i=1,j=1)、typed[1]='s' 长按(j=2)、a 匹配(i=2,j=3)、typed[3]='a' 长按(j=4)、e 匹配(i=3,j=5)。第 6 步时 name[3] = 'e'typed[5] = 'd' 不同,且 typed[5] = 'd'typed[4] = 'e' 也不同——这个 d 无法解释成长按,返回 false。正确,因为 namee 出现了两次而 typed 里只打出一次。

最后看收口那一句的必要性:name = "alex"typed = "ale"。三步正常匹配后 i = 3j = 3,循环退出。若直接 return true 会答错;判 i == 4 不成立,正确返回 false。

代码实现

class Solution {
    public boolean isLongPressedName(String name, String typed) {
        int i = 0;
        int j = 0;

        while (j < typed.length()) {
            if (i < name.length() && name.charAt(i) == typed.charAt(j)) {
                i++;
                j++;
            } else if (j > 0 && typed.charAt(j) == typed.charAt(j - 1)) {
                j++;
            } else {
                return false;
            }
        }

        return i == name.length();
    }
}
func isLongPressedName(name string, typed string) bool {
    i, j := 0, 0

    for j < len(typed) {
        if i < len(name) && name[i] == typed[j] {
            i++
            j++
        } else if j > 0 && typed[j] == typed[j-1] {
            j++
        } else {
            return false
        }
    }

    return i == len(name)
}

复杂度分析

  • 时间复杂度:$O(m + n)$,mname 长度、ntyped 长度。ij 都只增不减,每轮循环至少推进 j 一格,全程不回退。
  • 空间复杂度:$O(1)$。只用了两个下标,判定长按靠比较 typed[j]typed[j-1],不需要预先分组或统计次数。

关键点总结

  • 「A 能否由 B 经过某种单调变换得到」这类题,几乎都能用双指针一趟扫完:一个指针追踪源串的匹配进度,另一个负责解释目标串的每个字符。
  • 三个分支的优先级决定正确性:能正常匹配就优先匹配,匹配不上才考虑长按,都不行才否定。顺序一换就会出现该推进 i 时没推进的错配。
  • 判断长按不需要统计连续次数,只看「与前一个字符是否相同」即可——把分组信息即用即弃,省掉一趟预处理和 $O(n)$ 空间。
  • 循环以 typed 为主导(每个字符都必须被解释),退出后再单独校验 name 是否用完;两个串的结束条件必须分开处理,这是本题最主要的失分点。
  • 越界保护要写在短路与的左侧:i < name.length() && ...j > 0 && ...,顺序写反会在 name 提前用完或 j = 0 时直接崩溃。
  • 面试视角:写完立刻用 "alex"/"ale"name 没用完)、"a"/"ab"typed 有非法尾巴)、"alex"/"alex"(零次长按)三个用例口述验证,比讲复杂度更能体现边界意识。

易错点总结

  • 循环结束后直接 return truename = "alex"typed = "ale" 会返回 true,正确答案是 false——name 里的 x 根本没被打出来。
  • 循环条件写成 i < name.length()name = "a"typed = "ab"i 走完就退出,尾部非法的 b 从未被检查,误返回 true。
  • 两个分支的判断顺序颠倒:先判 typed[j] == typed[j-1] 再判正常匹配,name = "aa"typed = "aa" 中第二个 a 会被当成长按而不推进 i,最终 i = 1 != 2,误返回 false。
  • i < name.length() 的保护漏写或写在右侧name = "a"typed = "aa"i 变成 1 后再执行 name.charAt(1),抛出下标越界异常。
  • j > 0 的保护漏写name = "b"typed = "a"j = 0 时访问 typed.charAt(-1),直接异常;正确行为是返回 false。
  • 长按分支里顺手 i++name = "alex"typed = "aaleex" 中第 2 步会把 i 推到 2,字母 l 被跳过,后续匹配全部错位,误返回 false。
  • typed[j]name[i-1] 比较来判长按name = "ab"typed = "abb" 侥幸正确,但 name = "ab"typed = "aab" 中第 2 步 typed[1]='a'name[0]='a' 相同也会被判为长按(结果碰巧对),一旦 name 中出现 "aba" 这类模式就会误判;判定长按的依据应当是 typed 内部的连续性。
  • 先比较长度,typed.length() < name.length() 时提前返回 false 却漏掉其余检查:这一步本身没错(长按只会变长),但若把它当作充分条件写成「长度够就返回 true」,name = "ab"typed = "cd" 会误判为真。
  • 用「nametyped 的子序列」来判定name = "ab"typed = "acb"ab 确实是子序列,但多出的 c 不是长按产生的,正确答案是 false。子序列条件太松。
  • 用「排序后相等」或「字符计数相同」判定:长按会改变字符出现次数,name = "alex"typed = "aaleex" 的计数并不相同,这类做法从根上就不成立。
  • 误以为 typed 必须严格长于 name:题目问的是「可能」,零次长按也合法,name = "alex"typed = "alex" 应返回 true。

相似题目

题目 难度 考察点
392. 判断子序列 简单 允许任意跳过而非仅重复,条件比本题松,只需单向推进不必判非法字符
844. 比较含退格的字符串 简单 双串双指针但要从右往左扫,因为退格的作用方向朝左
28. 找出字符串中第一个匹配项的下标 简单 要求连续完全匹配,失配后主串指针需回退或用 KMP 的 next 数组
443. 压缩字符串 中等 同样按「连续相同字符」分段,但要把段长回写进原数组
27. 移除元素 简单 快慢指针的最简形态,写指针只在保留元素时推进,可与本题的 i 类比