Files
codetutor/backend/data/questions/backspace-string-compare.yaml

215 lines
9.6 KiB
YAML

title: Backspace String Compare
slug: backspace-string-compare
difficulty: easy
leetcode_id: 844
leetcode_url: https://leetcode.com/problems/backspace-string-compare/
categories:
- strings
- stack
- two-pointers
patterns:
- slug: two-pointers
is_optimal: true
function_signature: "def backspace_compare(s: str, t: str) -> bool:"
test_cases:
visible:
- input: { s: "ab#c", t: "ad#c" }
expected: true
- input: { s: "ab##", t: "c#d#" }
expected: true
- input: { s: "a#c", t: "b" }
expected: false
hidden:
- input: { s: "a", t: "a" }
expected: true
- input: { s: "###", t: "" }
expected: true
- input: { s: "a##b", t: "b" }
expected: true
- input: { s: "xywrrmp", t: "xywrrmu#p" }
expected: true
description: |
Given two strings `s` and `t`, return `true` *if they are equal when both are typed into empty text editors*. `'#'` means a backspace character.
Note that after backspacing an empty text, the text will continue empty.
constraints: |
- `1 <= s.length, t.length <= 200`
- `s` and `t` only contain lowercase letters and `'#'` characters
examples:
- input: 's = "ab#c", t = "ad#c"'
output: "true"
explanation: "Both s and t become \"ac\"."
- input: 's = "ab##", t = "c#d#"'
output: "true"
explanation: "Both s and t become \"\"."
- input: 's = "a#c", t = "b"'
output: "false"
explanation: "s becomes \"c\" while t becomes \"b\"."
explanation:
intuition: |
Imagine typing on a keyboard where `#` is the backspace key. Each time you press backspace, the last character you typed gets deleted. If the text is already empty, nothing happens.
The simplest mental model is to **simulate the typing process**: walk through each string character by character, building up the final result. When you see a letter, add it; when you see `#`, remove the last letter (if any).
However, there's a more elegant approach that uses **O(1) space**: process both strings **from right to left**. Why right to left? Because a backspace only affects characters *before* it, not after. By starting from the end, when we encounter a `#`, we know exactly how many characters to skip. We can then compare the "valid" characters of both strings one at a time without building the full result.
Think of it like this: instead of simulating forward typing, we're **rewinding** from the final cursor position, skipping over characters that would have been deleted.
approach: |
We solve this using a **Two Pointers (Reverse Traversal) Approach**:
**Step 1: Initialise pointers and skip counters**
- `i`: Pointer starting at the end of string `s` (index `len(s) - 1`)
- `j`: Pointer starting at the end of string `t` (index `len(t) - 1`)
- `skip_s`, `skip_t`: Counters tracking how many characters to skip in each string
&nbsp;
**Step 2: Process both strings from right to left**
- While either pointer is valid (>= 0):
- **Handle skips in `s`**: If current char is `#`, increment `skip_s` and move left. If `skip_s > 0` and current char isn't `#`, decrement `skip_s` and move left (skip this char)
- **Handle skips in `t`**: Same logic with `skip_t` and pointer `j`
- After skipping, compare the characters at positions `i` and `j`
&nbsp;
**Step 3: Compare characters**
- If both pointers are valid, characters must match
- If one pointer is valid and the other isn't, strings differ in length
- Move both pointers left and continue
&nbsp;
**Step 4: Return result**
- If we process both strings completely with all characters matching, return `true`
- Return `false` if any mismatch occurs
&nbsp;
This approach works because backspaces only affect preceding characters. By going backwards and counting skips, we can identify which characters "survive" without actually building the result strings.
common_pitfalls:
- title: Building Full Strings (Wasteful Space)
description: |
A common first approach is to build the final string for both `s` and `t` using a stack or list, then compare them.
While this works and is O(n) time, it uses **O(n) space**. The follow-up challenge asks for O(1) space, which the two-pointer approach achieves.
For interviews, mention both approaches: stack-based for clarity, two-pointer for optimal space.
wrong_approach: "Build both result strings, then compare"
correct_approach: "Compare character-by-character using reverse traversal"
- title: Processing Left to Right
description: |
Trying to process strings left-to-right with O(1) space is tricky because you don't know how many future backspaces will delete current characters.
For example, in `"abc###"`, when you see `'a'`, you don't know yet that it will be deleted by a later `#`. You'd need to look ahead or store characters.
Going **right to left** avoids this: when you see `#`, you immediately know to skip the next valid character to the left.
wrong_approach: "Left-to-right with lookahead"
correct_approach: "Right-to-left with skip counter"
- title: Forgetting Edge Cases with Multiple Backspaces
description: |
Consider `"ab##"`: both characters get deleted. Or `"a###"`: one character deleted, two backspaces on empty text (no effect).
Your skip counter must handle:
- Multiple consecutive `#` characters (accumulate skips)
- More backspaces than characters (skips that have nothing to delete)
- Empty result strings (both pointers go past 0)
key_takeaways:
- "**Reverse traversal pattern**: When an operation affects preceding elements (like backspace), consider processing from the end"
- "**Two-pointer comparison**: Instead of building and comparing, compare elements one at a time using coordinated pointers"
- "**Space optimisation**: The stack approach is intuitive (O(n) space), but the two-pointer approach achieves O(1) space"
- "**Skip counter technique**: Track \"pending operations\" as a counter to avoid storing intermediate results"
time_complexity: "O(n + m). We traverse each string at most twice (once for skipping, once for comparing), where `n` and `m` are the lengths of `s` and `t`."
space_complexity: "O(1) for two-pointer approach. We only use a constant number of variables regardless of input size. The stack approach uses O(n + m) space."
solutions:
- approach_name: Two Pointers (Reverse Traversal)
is_optimal: true
code: |
def backspace_compare(s: str, t: str) -> bool:
# Start from the end of both strings
i, j = len(s) - 1, len(t) - 1
skip_s = skip_t = 0
# Process both strings from right to left
while i >= 0 or j >= 0:
# Find the next valid character in s (after applying backspaces)
while i >= 0:
if s[i] == '#':
skip_s += 1 # Count backspace
i -= 1
elif skip_s > 0:
skip_s -= 1 # Skip this character (it's deleted)
i -= 1
else:
break # Found a valid character
# Find the next valid character in t (after applying backspaces)
while j >= 0:
if t[j] == '#':
skip_t += 1 # Count backspace
j -= 1
elif skip_t > 0:
skip_t -= 1 # Skip this character (it's deleted)
j -= 1
else:
break # Found a valid character
# Compare the valid characters
if i >= 0 and j >= 0:
if s[i] != t[j]:
return False # Characters don't match
elif i >= 0 or j >= 0:
return False # One string has characters left, other doesn't
# Move to next characters
i -= 1
j -= 1
return True # All characters matched
explanation: |
**Time Complexity:** O(n + m) — Each character in both strings is visited at most twice.
**Space Complexity:** O(1) — Only a constant number of variables used.
We traverse both strings from right to left, using skip counters to track pending backspaces. When we find a `#`, we increment the skip counter. When we find a regular character with pending skips, we decrement and continue. Otherwise, we've found a "surviving" character to compare. This elegantly handles the temporal nature of backspaces without building intermediate strings.
- approach_name: Stack Simulation
is_optimal: false
code: |
def backspace_compare(s: str, t: str) -> bool:
def build_string(string: str) -> str:
"""Simulate typing and return final result."""
stack = []
for char in string:
if char == '#':
if stack: # Only pop if stack not empty
stack.pop()
else:
stack.append(char)
return ''.join(stack)
# Build both final strings and compare
return build_string(s) == build_string(t)
explanation: |
**Time Complexity:** O(n + m) — Single pass through each string.
**Space Complexity:** O(n + m) — Stack can hold up to all characters.
This approach directly simulates the typing process using a stack. Letters get pushed, `#` causes a pop (if possible). While not space-optimal, this solution is very intuitive and easy to understand. It's a good first answer in an interview before optimising to the two-pointer approach.