LeetCode 844 - 比较含退格的字符串
问题描述
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。
示例
示例 1:
1 2 3
| 输入:s = "ab#c", t = "ad#c" 输出:true 解释:s 和 t 都会变成 "ac"。
|
示例 2:
1 2 3
| 输入:s = "ab##", t = "c#d#" 输出:true 解释:s 和 t 都会变成 ""。
|
示例 3:
1 2 3
| 输入:s = "a#c", t = "b" 输出:false 解释:s 会变成 "c",但 t 仍然是 "b"。
|
约束条件
1 <= s.length, t.length <= 200
s 和 t 只含有小写字母以及字符 '#'
进阶要求
你可以用 O(n) 的时间复杂度和 O(1) 的空间复杂度解决该问题吗?
解题思路
这道题的核心是模拟退格操作,然后比较两个字符串是否相等。有两种主要思路:
方法一:栈模拟(直观解法)
核心思想:使用栈来模拟文本编辑器的行为
- 遇到普通字符:入栈
- 遇到
#:出栈(如果栈不为空)
- 最后比较两个栈的内容
方法二:双指针逆向遍历(最优解法)
核心思想:从后往前遍历,跳过被退格的字符
- 维护两个指针分别从两个字符串的末尾开始
- 遇到
# 时,跳过下一个有效字符
- 比较当前有效字符是否相等
实现细节
栈模拟法
构建有效字符串:
- 遍历字符串,遇到普通字符就加入栈
- 遇到
# 就从栈中弹出字符(如果栈不为空)
比较结果:
双指针法
逆向遍历:
- 从字符串末尾开始向前遍历
- 遇到
# 时,记录需要跳过的字符数量
跳过被退格的字符:
- 当
skip 计数大于 0 时,跳过当前字符
- 遇到
# 时,skip 计数加 1
比较有效字符:
代码实现
方法一:栈模拟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public boolean backspaceCompare(String s, String t) { return buildString(s).equals(buildString(t)); }
private String buildString(String str) { Stack<Character> stack = new Stack<>(); for (char c : str.toCharArray()) { if (c != '#') { stack.push(c); } else if (!stack.isEmpty()) { stack.pop(); } } return String.valueOf(stack); }
|
方法二:双指针(最优解)
核心思想
双指针方法的核心思想是从后往前遍历,这样可以避免处理退格字符的复杂性。当我们从后往前遍历时,遇到 # 就记录需要跳过的字符数量,然后跳过相应数量的字符,直到找到下一个有效字符进行比较。
算法步骤
- 初始化指针:两个指针分别指向两个字符串的末尾
- 逆向遍历:从右往左遍历两个字符串
- 处理退格:遇到
# 时记录跳过计数,遇到普通字符时根据跳过计数决定是否跳过
- 比较字符:找到两个字符串的下一个有效字符进行比较
- 边界处理:处理字符串长度不同的情况
详细实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public boolean backspaceCompare(String s, String t) { int i = s.length() - 1; int j = t.length() - 1; while (i >= 0 || j >= 0) { int skipS = 0; while (i >= 0) { if (s.charAt(i) == '#') { skipS++; i--; } else if (skipS > 0) { skipS--; i--; } else { break; } } int skipT = 0; while (j >= 0) { if (t.charAt(j) == '#') { skipT++; j--; } else if (skipT > 0) { skipT--; j--; } else { break; } } if (i >= 0 && j >= 0) { if (s.charAt(i) != t.charAt(j)) { return false; } } else if (i >= 0 || j >= 0) { return false; } i--; j--; } return true; }
|
代码逐步执行样例
以 s = "ab#c", t = "ad#c" 为例,详细展示双指针的执行过程:
初始状态:
1 2 3
| s = "ab#c" (索引: 0,1,2,3) t = "ad#c" (索引: 0,1,2,3) i = 3, j = 3
|
第1轮循环:
1 2 3 4 5 6
| i = 3, j = 3 s[3] = 'c', t[3] = 'c' 都不是 '#',skipS = 0, skipT = 0 找到有效字符:s[3] = 'c', t[3] = 'c' 比较:'c' == 'c' ✓ i = 2, j = 2
|
第2轮循环:
1 2 3 4 5 6 7 8 9 10 11 12 13
| i = 2, j = 2 s[2] = '#', t[2] = '#' 都是退格字符: - s: skipS = 1, i = 1 - t: skipT = 1, j = 1 继续处理: - s: s[1] = 'b', skipS = 1 > 0,跳过,skipS = 0, i = 0 - t: t[1] = 'd', skipT = 1 > 0,跳过,skipT = 0, j = 0 继续处理: - s: s[0] = 'a', skipS = 0,找到有效字符 'a' - t: t[0] = 'a', skipT = 0,找到有效字符 'a' 比较:'a' == 'a' ✓ i = -1, j = -1
|
第3轮循环:
1 2 3
| i = -1, j = -1 i < 0 && j < 0,循环结束 返回 true
|
执行过程可视化:
1 2 3 4 5
| 步骤1: 比较 'c' 和 'c' → 相等 步骤2: 遇到 '#' 和 '#' → 跳过下一个字符 步骤3: 跳过 'b' 和 'd' → 继续向前 步骤4: 比较 'a' 和 'a' → 相等 结果: 两个字符串都变成 "ac" → 返回 true
|
边界情况处理
情况1:全为退格字符
1 2 3 4 5
| s = "###", t = "##" 执行过程: - s: 跳过所有字符,最终 i = -1 - t: 跳过所有字符,最终 j = -1 - 两个字符串都为空,返回 true
|
情况2:一个字符串更长
1 2 3
| s = "a#c", t = "b" 执行过程: - 第1轮:比较 'c' 和 'b' → 不相等,返回 false
|
情况3:连续退格
1 2 3 4 5
| s = "ab##c", t = "a#c" 执行过程: - s: 遇到 "##",跳过 "b" 和 "a",有效字符为 'c' - t: 遇到 "#",跳过 'a',有效字符为 'c' - 比较 'c' 和 'c' → 相等,返回 true
|
执行过程示例
以 s = "ab#c", t = "ad#c" 为例:
栈模拟法执行过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| s = "ab#c": - 'a' → stack: ['a'] - 'b' → stack: ['a', 'b'] - '#' → stack: ['a'] (弹出 'b') - 'c' → stack: ['a', 'c'] 结果: "ac"
t = "ad#c": - 'a' → stack: ['a'] - 'd' → stack: ['a', 'd'] - '#' → stack: ['a'] (弹出 'd') - 'c' → stack: ['a', 'c'] 结果: "ac"
比较: "ac" == "ac" → true
|
双指针法执行过程:
1 2 3 4 5 6
| 初始: i=3, j=3 s[3]='c', t[3]='c' → 相等,i=2, j=2 s[2]='#', t[2]='#' → 都是退格,跳过下一个字符 s[1]='b', t[1]='d' → 被跳过 s[0]='a', t[0]='a' → 相等 结果: true
|
方法比较
| 方面 |
栈模拟法 |
双指针法 |
| 时间复杂度 |
O(n + m) |
O(n + m) |
| 空间复杂度 |
O(n + m) |
O(1) |
| 优点 |
思路直观,易于理解 |
空间效率高,符合进阶要求 |
| 缺点 |
需要额外空间 |
实现相对复杂 |
| 推荐度 |
★★★☆☆ |
★★★★★ |
复杂度分析
栈模拟法
- 时间复杂度:$O(n + m)$,其中 n 和 m 分别是两个字符串的长度
- 空间复杂度:$O(n + m)$,需要两个栈来存储字符
双指针法
- 时间复杂度:$O(n + m)$,每个字符最多被访问一次
- 空间复杂度:$O(1)$,只使用了常数额外空间
关键收获
- 双指针技巧:当需要从后往前处理字符串时,双指针是很好的选择
- 退格模拟:遇到退格字符时,需要跳过下一个有效字符
- 边界处理:注意处理字符串长度不同的情况
- 空间优化:在满足时间复杂度要求的前提下,尽量优化空间复杂度
常见陷阱
- 空字符串处理:当字符串全为
# 时,结果应该是空字符串
- 指针越界:确保在访问字符前检查索引是否有效
- 跳过逻辑:正确处理连续的
# 字符
相关问题