Facebook Pixel

1209. Remove All Adjacent Duplicates in String II

MediumStackString
Leetcode Link

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Initialize: Create an empty stack t and set up pointers i = 0 and n = len(s).

  2. Group consecutive characters:

    • Starting from position i, find position j where all characters from i to j-1 are the same
    • Calculate the count: cnt = j - i
    • Apply modulo: cnt %= k to get the remainder after removing groups of k
  3. 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
    • 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
  4. Move to next group: Set i = j and repeat until we've processed the entire string.

  5. 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 Evaluator

Example 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More