1209. Remove All Adjacent Duplicates in String II
Problem Description
You are given a string s
and an integer k
. Your task is to repeatedly remove groups of k
consecutive identical characters from the string until no more such removals can be made.
A k duplicate removal means finding k
adjacent characters that are all the same and removing them from the string. After removal, the parts of the string on the left and right sides of the removed substring are joined together.
For example, if s = "deeedbbcccbdaa"
and k = 3
:
- First, you can remove
"eee"
to get"dddbbcccbdaa"
- Then remove
"ddd"
to get"bbcccbdaa"
- Then remove
"ccc"
to get"bbbdaa"
- Then remove
"bbb"
to get"daa"
- No more removals of 3 consecutive identical characters can be made
The process continues until you cannot find any group of k
consecutive identical characters to remove. The problem guarantees that the final result is unique regardless of the order of removals.
Your goal is to return the final string after all possible k duplicate removals have been completed.
Intuition
The key insight is that we need to handle cascading removals - when we remove k
characters, the parts before and after might combine to form a new group of k
identical characters. A naive approach of repeatedly scanning the string would be inefficient.
Think about how characters group together. When we see consecutive identical characters, we can count them. If we have exactly k
(or a multiple of k
), they all disappear. If we have more than k
but not a multiple of k
, some characters remain.
The crucial observation is that we can use a stack to track characters and their counts efficiently. Why a stack? Because when we remove characters from the middle of the string, the characters on the left and right sides come together - this is exactly the behavior we get when we pop from a stack and the previous element becomes accessible again.
As we traverse the string, we group consecutive identical characters and calculate how many would remain after removing groups of k
. If the remaining count matches with the character at the top of the stack, we can merge them. This is because in the final string, these characters would be adjacent.
For example, if we have "abbba"
with k=2
, we process:
'a'
with count 1 → stack:[('a', 1)]
'bbb'
gives count 3, which mod 2 = 1 → stack:[('a', 1), ('b', 1)]
'a'
with count 1, same as stack top's character → merge to get('a', 2)
- Since 2 % 2 = 0, remove it → stack becomes empty
This approach processes the string in a single pass while handling all the cascading effects through the stack's natural LIFO behavior.
Learn more about Stack patterns.
Solution Approach
The solution uses a stack-based approach to efficiently handle the k-duplicate removals. Here's how the implementation works:
Data Structure: We use a list t
as our stack, where each element is a pair [character, count]
representing a character and how many consecutive occurrences of it we have.
Algorithm Steps:
-
Initialize: Create an empty stack
t
and set up pointersi = 0
andn = len(s)
. -
Group consecutive characters:
- Starting from position
i
, find positionj
where all characters fromi
toj-1
are the same - Calculate the count:
cnt = j - i
- Apply modulo:
cnt %= k
to get the remainder after removing groups ofk
- Starting from position
-
Process the group:
- Case 1 - Merge with stack top: If the stack is not empty and the top character matches the current character
s[i]
:- Add the current count to the top element's count:
t[-1][1] = (t[-1][1] + cnt) % k
- If the new count becomes 0 (perfectly divisible by
k
), pop the element from stack
- Add the current count to the top element's count:
- Case 2 - Add new element: If the current character differs from the stack top (or stack is empty) and
cnt > 0
:- Push
[s[i], cnt]
onto the stack
- Push
- Case 1 - Merge with stack top: If the stack is not empty and the top character matches the current character
-
Move to next group: Set
i = j
and repeat until we've processed the entire string. -
Build result: After processing all characters, construct the final string by concatenating each character in the stack repeated by its count:
ans = [c * v for c, v in t]
Example Walkthrough with s = "deeedbbcccbdaa"
, k = 3
:
- Process
'd'
(count=1) → stack:[['d', 1]]
- Process
'eee'
(count=3, 3%3=0) → nothing added to stack - Process
'd'
(count=1) → merges with stack top →[['d', 2]]
- Process
'bb'
(count=2) → stack:[['d', 2], ['b', 2]]
- Process
'ccc'
(count=3, 3%3=0) → nothing added - Process
'b'
(count=1) → merges with'b'
→[['d', 2], ['b', 3]]
→ 3%3=0, pop →[['d', 2]]
- Process
'd'
(count=1) → merges →[['d', 3]]
→ 3%3=0, pop →[]
- Process
'aa'
(count=2) → stack:[['a', 2]]
- Result:
"aa"
The time complexity is O(n)
where n
is the length of the string, as we process each character once. The space complexity is O(n)
for the stack in the worst case.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a simpler example with s = "abbaca"
and k = 3
:
Initial Setup:
- Stack
t = []
(empty) - We'll process groups of consecutive identical characters
Step 1: Process 'a' (position 0)
- Group: just one 'a', count = 1
- 1 % 3 = 1 (remainder after removing groups of 3)
- Stack is empty, so push
['a', 1]
- Stack:
[['a', 1]]
Step 2: Process 'bb' (positions 1-2)
- Group: two 'b's, count = 2
- 2 % 3 = 2 (can't remove any group of 3)
- Current character 'b' ≠ stack top 'a', so push
['b', 2]
- Stack:
[['a', 1], ['b', 2]]
Step 3: Process 'a' (position 3)
- Group: one 'a', count = 1
- 1 % 3 = 1
- Current character 'a' ≠ stack top 'b', so push
['a', 1]
- Stack:
[['a', 1], ['b', 2], ['a', 1]]
Step 4: Process 'c' (position 4)
- Group: one 'c', count = 1
- 1 % 3 = 1
- Current character 'c' ≠ stack top 'a', so push
['c', 1]
- Stack:
[['a', 1], ['b', 2], ['a', 1], ['c', 1]]
Step 5: Process 'a' (position 5)
- Group: one 'a', count = 1
- 1 % 3 = 1
- Current character 'a' ≠ stack top 'c', so push
['a', 1]
- Stack:
[['a', 1], ['b', 2], ['a', 1], ['c', 1], ['a', 1]]
Build Result:
- Concatenate: 'a' × 1 + 'b' × 2 + 'a' × 1 + 'c' × 1 + 'a' × 1
- Final string:
"abbaca"
Now let's see a case where merging happens with s = "abbbaca"
and k = 3
:
Step 1: Process 'a' (position 0)
- Stack:
[['a', 1]]
Step 2: Process 'bbb' (positions 1-3)
- Group: three 'b's, count = 3
- 3 % 3 = 0 (perfectly divisible, nothing to add!)
- Stack remains:
[['a', 1]]
Step 3: Process 'a' (position 4)
- Group: one 'a', count = 1
- Current character 'a' = stack top 'a', so merge!
- New count: (1 + 1) % 3 = 2
- Update stack top:
[['a', 2]]
Step 4: Process 'c' (position 5)
- Stack:
[['a', 2], ['c', 1]]
Step 5: Process 'a' (position 6)
- Stack:
[['a', 2], ['c', 1], ['a', 1]]
Result: "aaca"
The key insight: when 'bbb' disappears, the 'a' characters on either side come together and merge in our stack!
Solution Implementation
1class Solution:
2 def removeDuplicates(self, s: str, k: int) -> str:
3 # Stack to store [character, count] pairs
4 # This helps track consecutive occurrences of characters
5 stack = []
6
7 # Iterate through the string using two pointers
8 current_index = 0
9 string_length = len(s)
10
11 while current_index < string_length:
12 # Find the end of the current sequence of identical characters
13 next_index = current_index
14 while next_index < string_length and s[next_index] == s[current_index]:
15 next_index += 1
16
17 # Calculate how many consecutive characters we found
18 consecutive_count = next_index - current_index
19 # Keep only the remainder after removing groups of k
20 remaining_count = consecutive_count % k
21
22 # Check if we can merge with the previous character in stack
23 if stack and stack[-1][0] == s[current_index]:
24 # Merge with previous occurrence and apply modulo k
25 stack[-1][1] = (stack[-1][1] + remaining_count) % k
26 # If count becomes 0, remove the character completely
27 if stack[-1][1] == 0:
28 stack.pop()
29 elif remaining_count > 0:
30 # Add new character with its count to stack
31 stack.append([s[current_index], remaining_count])
32
33 # Move to the next different character
34 current_index = next_index
35
36 # Build the final result string
37 result_parts = [character * count for character, count in stack]
38 return "".join(result_parts)
39
1class Solution {
2 public String removeDuplicates(String s, int k) {
3 // Stack to store character and its consecutive count
4 // Each element is [characterIndex, count]
5 Deque<int[]> stack = new ArrayDeque<>();
6
7 // Process each character in the string
8 for (int i = 0; i < s.length(); i++) {
9 // Convert character to index (0-25 for 'a'-'z')
10 int charIndex = s.charAt(i) - 'a';
11
12 // Check if current character matches the top of stack
13 if (!stack.isEmpty() && stack.peek()[0] == charIndex) {
14 // Increment count and use modulo to reset when reaching k
15 stack.peek()[1] = (stack.peek()[1] + 1) % k;
16
17 // If count becomes 0 (k consecutive duplicates found), remove from stack
18 if (stack.peek()[1] == 0) {
19 stack.pop();
20 }
21 } else {
22 // Different character, push new entry with count 1
23 stack.push(new int[] {charIndex, 1});
24 }
25 }
26
27 // Build result string from remaining characters in stack
28 StringBuilder result = new StringBuilder();
29
30 // Iterate through stack elements (bottom to top due to iterator)
31 for (int[] element : stack) {
32 // Convert index back to character
33 char character = (char) (element[0] + 'a');
34 int count = element[1];
35
36 // Append character 'count' times
37 for (int i = 0; i < count; i++) {
38 result.append(character);
39 }
40 }
41
42 // Reverse to get correct order (stack is LIFO)
43 result.reverse();
44
45 return result.toString();
46 }
47}
48
1class Solution {
2public:
3 string removeDuplicates(string s, int k) {
4 // Stack to store pairs of (character, consecutive count)
5 vector<pair<char, int>> stack;
6
7 // Iterate through each character in the string
8 for (char& ch : s) {
9 // Check if stack is not empty and top character matches current character
10 if (!stack.empty() && stack.back().first == ch) {
11 // Increment the count of consecutive characters
12 stack.back().second++;
13
14 // If count reaches k, remove this group entirely
15 if (stack.back().second == k) {
16 stack.pop_back();
17 }
18 } else {
19 // Different character encountered, push new pair with count 1
20 stack.push_back({ch, 1});
21 }
22 }
23
24 // Build the result string from remaining characters in stack
25 string result;
26 for (const auto& [character, count] : stack) {
27 // Append 'count' number of 'character' to result
28 result.append(count, character);
29 }
30
31 return result;
32 }
33};
34
1function removeDuplicates(s: string, k: number): string {
2 // Stack to store pairs of [character, consecutive count]
3 const stack: [string, number][] = [];
4
5 // Iterate through each character in the string
6 for (const ch of s) {
7 // Check if stack is not empty and top character matches current character
8 if (stack.length > 0 && stack[stack.length - 1][0] === ch) {
9 // Increment the count of consecutive characters
10 stack[stack.length - 1][1]++;
11
12 // If count reaches k, remove this group entirely
13 if (stack[stack.length - 1][1] === k) {
14 stack.pop();
15 }
16 } else {
17 // Different character encountered, push new pair with count 1
18 stack.push([ch, 1]);
19 }
20 }
21
22 // Build the result string from remaining characters in stack
23 let result = '';
24 for (const [character, count] of stack) {
25 // Append 'count' number of 'character' to result
26 result += character.repeat(count);
27 }
28
29 return result;
30}
31
Time and Space Complexity
Time Complexity: O(n)
The algorithm uses a single while loop that iterates through the string once. In the outer while loop, the variable i
starts at 0 and moves to n
through jumps determined by the inner while loop. The inner while loop finds consecutive duplicate characters, but each character in the string is visited exactly once across all iterations (the inner loop advances j
and then i
jumps to j
). All operations inside the loops - including list append, pop, and accessing the last element - are O(1)
operations. Therefore, the overall time complexity is O(n)
where n
is the length of the input string s
.
Space Complexity: O(n)
The space complexity is determined by the stack t
which stores character-count pairs. In the worst case, when there are no removals (all characters are different or no group reaches size k
), the stack could contain up to n
characters. The final answer string ans
also requires O(n)
space in the worst case. Therefore, the total space complexity is O(n)
where n
is the length of the input string s
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Handling Character Merging Correctly
The Problem: A critical mistake is forgetting to apply the modulo operation when merging counts with the stack top. Without this, consecutive groups won't be properly removed.
Incorrect Implementation:
if stack and stack[-1][0] == s[current_index]: stack[-1][1] += remaining_count # Wrong! Missing modulo if stack[-1][1] >= k: # Wrong condition! stack[-1][1] -= k
Why It Fails: Consider s = "aabbaaa"
with k = 3
. After processing "aa", "bb", and "aaa", the stack would incorrectly contain [['a', 5]]
instead of properly removing groups of 3.
Correct Implementation:
if stack and stack[-1][0] == s[current_index]: stack[-1][1] = (stack[-1][1] + remaining_count) % k if stack[-1][1] == 0: stack.pop()
Pitfall 2: Processing Characters One by One Instead of in Groups
The Problem: Iterating through each character individually makes the logic more complex and prone to errors.
Incorrect Approach:
for char in s: if stack and stack[-1][0] == char: stack[-1][1] += 1 if stack[-1][1] == k: stack.pop() else: stack.append([char, 1])
Why It Fails: This approach doesn't handle pre-existing groups correctly. For s = "aaabbb"
with k = 3
, it would process each 'a' individually and remove them, but it doesn't efficiently handle the initial group of 3 'a's.
Better Approach: Group consecutive characters first, then process each group as a whole with modulo operation.
Pitfall 3: Incorrectly Building the Final Result
The Problem: Using string concatenation in a loop instead of joining a list can lead to inefficiency.
Inefficient Implementation:
result = "" for character, count in stack: result += character * count # String concatenation in loop return result
Optimal Implementation:
result_parts = [character * count for character, count in stack] return "".join(result_parts)
This avoids creating multiple intermediate string objects and is more efficient for larger inputs.
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!