LeetCode 1910 - Remove All Occurrences of a Substring
The problem asks us to repeatedly remove occurrences of a substring part from a string s. The important detail is that on every operation, we must remove the leftmost occurrence of part. We continue performing removals until part no longer appears anywhere inside s.
Difficulty: 🟡 Medium
Topics: String, Stack, Simulation
Solution
Problem Understanding
The problem asks us to repeatedly remove occurrences of a substring part from a string s. The important detail is that on every operation, we must remove the leftmost occurrence of part. We continue performing removals until part no longer appears anywhere inside s.
In simpler terms, we scan the string, remove one matching substring at a time, and keep doing this until the string no longer contains the target pattern.
For example, if:
s = "daabcbaabcbc"
part = "abc"
the first "abc" appears starting at index 2, so removing it produces:
"dabaabcbc"
After the removal, new occurrences may appear because characters that were previously separated can now become adjacent. This is the key challenge in the problem.
The input consists of two lowercase English strings:
s, the original stringpart, the substring we repeatedly remove
The output is the final string after all removals are complete.
The constraints are:
1 <= s.length <= 1000
1 <= part.length <= 1000
These limits are relatively small, which means even moderately inefficient solutions may pass. However, the problem is designed to reward a cleaner and more efficient simulation approach.
Several edge cases are important:
- Removing one occurrence may create another occurrence across the boundary.
partmay overlap with itself.- The entire string may disappear.
partmay never appear at all.partcould be length1, which means every matching character must be removed.
A naive implementation can easily become inefficient if it repeatedly scans and rebuilds strings.
Approaches
Brute Force Approach
The most direct solution is to repeatedly search for the leftmost occurrence of part inside s.
At each iteration:
- Find the index of
partins - If no occurrence exists, stop
- Otherwise, remove that substring
- Repeat
In Python, this can be implemented using find() and string slicing.
This approach is correct because it exactly follows the rules described in the problem statement. Every iteration removes the leftmost occurrence until none remain.
However, repeatedly searching and rebuilding strings can become expensive. Each search may take O(n) time, and each removal creates a new string of size O(n). In the worst case, we may perform many removals.
Optimal Stack-Based Approach
The key observation is that we only need to check whether the most recently built portion of the string ends with part.
Instead of repeatedly rescanning the entire string, we process characters one by one and build the answer incrementally using a stack-like structure.
For every character:
- Append it to the current result
- Check whether the end of the current result matches
part - If it does, remove those characters immediately
This works because any new occurrence created by previous removals must appear near the boundary where new characters were added.
By continuously maintaining a valid partial result, we avoid repeated full-string rescanning.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Repeatedly searches and rebuilds strings |
| Optimal | O(n × m) | O(n) | Uses a stack and checks only recent characters |
Here:
n = len(s)m = len(part)
Because constraints are only 1000, the stack solution is easily fast enough.
Algorithm Walkthrough
Optimal Stack-Based Algorithm
- Create an empty stack structure.
We use the stack to store the characters of the current processed string. This allows efficient append and removal operations.
2. Iterate through each character in s.
We process the input left to right because the problem always removes the leftmost occurrence first. 3. Append the current character to the stack.
This simulates building the resulting string incrementally.
4. After appending, check whether the end of the stack matches part.
Since only newly appended characters can create a new occurrence, we only need to compare the suffix of the current stack with part.
5. If the suffix matches part, remove the last len(part) characters.
This simulates removing the substring immediately once it appears. 6. Continue processing until all characters are handled. 7. Join the remaining stack characters into the final string and return it.
Why it works
The algorithm maintains an invariant:
After processing each character, the stack contains the correct string after removing all occurrences of
partfrom the processed prefix.
Whenever a new occurrence of part appears, it must end at the current character because earlier occurrences would already have been removed. By immediately deleting matching suffixes, the stack always remains valid.
This guarantees that the final stack is exactly the desired answer.
Python Solution
class Solution:
def removeOccurrences(self, s: str, part: str) -> str:
stack = []
part_length = len(part)
for char in s:
stack.append(char)
if len(stack) >= part_length:
if "".join(stack[-part_length:]) == part:
del stack[-part_length:]
return "".join(stack)
The implementation uses a list as a stack because Python lists support efficient append and deletion from the end.
We first store the length of part to avoid recalculating it repeatedly.
For every character in s, we append it to the stack. After appending, we check whether the most recent characters form the substring part.
The expression:
stack[-part_length:]
extracts the suffix of the current stack. If it matches part, we remove those characters immediately using:
del stack[-part_length:]
At the end, we join all remaining characters into the final string.
This implementation directly follows the algorithm described earlier and efficiently maintains the partially processed result.
Go Solution
func removeOccurrences(s string, part string) string {
stack := make([]byte, 0)
partLen := len(part)
for i := 0; i < len(s); i++ {
stack = append(stack, s[i])
if len(stack) >= partLen {
match := true
for j := 0; j < partLen; j++ {
if stack[len(stack)-partLen+j] != part[j] {
match = false
break
}
}
if match {
stack = stack[:len(stack)-partLen]
}
}
}
return string(stack)
}
The Go implementation uses a []byte slice as the stack because strings in Go are immutable.
Appending characters is efficient with append, and removing the suffix is done using slice truncation:
stack = stack[:len(stack)-partLen]
Unlike Python, Go does not provide direct string slicing comparisons as conveniently, so we manually compare characters in a loop.
Since the problem only contains lowercase English letters, using bytes is completely safe and efficient.
Worked Examples
Example 1
s = "daabcbaabcbc"
part = "abc"
| Step | Character | Stack After Append | Match Found | Stack After Removal |
|---|---|---|---|---|
| 1 | d | d | No | d |
| 2 | a | da | No | da |
| 3 | a | daa | No | daa |
| 4 | b | daab | No | daab |
| 5 | c | daabc | Yes | da |
| 6 | b | dab | No | dab |
| 7 | a | daba | No | daba |
| 8 | a | dabaa | No | dabaa |
| 9 | b | dabaab | No | dabaab |
| 10 | c | dabaabc | Yes | daba |
| 11 | b | dabab | No | dabab |
| 12 | c | dababc | Yes | dab |
Final answer:
"dab"
Example 2
s = "axxxxyyyyb"
part = "xy"
| Step | Character | Stack After Append | Match Found | Stack After Removal |
|---|---|---|---|---|
| 1 | a | a | No | a |
| 2 | x | ax | No | ax |
| 3 | x | axx | No | axx |
| 4 | x | axxx | No | axxx |
| 5 | x | axxxx | No | axxxx |
| 6 | y | axxxxy | Yes | axxx |
| 7 | y | axxxy | Yes | axx |
| 8 | y | axxy | Yes | ax |
| 9 | y | axy | Yes | a |
| 10 | b | ab | No | ab |
Final answer:
"ab"
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n × m) | Each character may trigger a suffix comparison of length m |
| Space | O(n) | The stack may store the entire string |
The stack grows at most to the size of the input string, so the space complexity is linear.
For every character added, we may compare up to m characters against part. Therefore the total worst case runtime is O(n × m).
Given the constraints of at most 1000 characters, this solution is easily efficient enough.
Test Cases
solution = Solution()
# Provided examples
assert solution.removeOccurrences("daabcbaabcbc", "abc") == "dab" # multiple removals
assert solution.removeOccurrences("axxxxyyyyb", "xy") == "ab" # cascading removals
# No occurrence
assert solution.removeOccurrences("hello", "xyz") == "hello" # substring absent
# Entire string removed
assert solution.removeOccurrences("aaaa", "aa") == "" # repeated full removals
# Single character part
assert solution.removeOccurrences("banana", "a") == "bnn" # remove all matching chars
# Overlapping pattern behavior
assert solution.removeOccurrences("aaaaa", "aa") == "a" # overlapping removals
# Pattern equals string
assert solution.removeOccurrences("abc", "abc") == "" # exact full removal
# Pattern longer than string
assert solution.removeOccurrences("ab", "abcd") == "ab" # impossible match
# Repeated boundary formation
assert solution.removeOccurrences("abababa", "aba") == "b" # new matches form after removal
# Minimal input
assert solution.removeOccurrences("a", "a") == "" # smallest valid input
# Single remaining character
assert solution.removeOccurrences("abcabcxabc", "abc") == "x" # repeated removals around center
| Test | Why |
|---|---|
"daabcbaabcbc", "abc" |
Validates repeated removals |
"axxxxyyyyb", "xy" |
Validates cascading adjacent removals |
"hello", "xyz" |
Confirms unchanged result when no match exists |
"aaaa", "aa" |
Confirms complete deletion |
"banana", "a" |
Tests single-character pattern |
"aaaaa", "aa" |
Tests overlapping behavior |
"abc", "abc" |
Tests full-string removal |
"ab", "abcd" |
Tests pattern longer than string |
"abababa", "aba" |
Tests new matches forming after removal |
"a", "a" |
Tests smallest constraints |
"abcabcxabc", "abc" |
Tests multiple separated removals |
Edge Cases
One important edge case occurs when removing a substring creates a new occurrence across the boundary of the removed section. For example:
s = "abababa"
part = "aba"
After removing the first "aba", the remaining characters become adjacent and form another occurrence. A naive implementation that does not properly continue checking can miss this. The stack approach handles this naturally because every newly formed suffix is checked immediately.
Another tricky case is when part contains only one character. For example:
s = "banana"
part = "a"
Every occurrence must be removed independently. Some implementations incorrectly assume the substring length is greater than one. The stack method still works correctly because suffix checking with length 1 is valid.
A third important edge case is overlapping patterns such as:
s = "aaaaa"
part = "aa"
The occurrences overlap heavily. If removals are not processed carefully in left-to-right order, incorrect results may occur. The stack solution correctly removes the leftmost occurrence as soon as it forms, preserving the intended behavior defined by the problem statement.