questions B (backspace - burst-balloons)
This commit is contained in:
193
backend/data/questions/backspace-string-compare.yaml
Normal file
193
backend/data/questions/backspace-string-compare.yaml
Normal file
@@ -0,0 +1,193 @@
|
||||
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:
|
||||
- two-pointers
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
**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`
|
||||
|
||||
|
||||
|
||||
**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
|
||||
|
||||
|
||||
|
||||
**Step 4: Return result**
|
||||
|
||||
- If we process both strings completely with all characters matching, return `true`
|
||||
- Return `false` if any mismatch occurs
|
||||
|
||||
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user