Remove All Adjacent Duplicates in String II

Medium Stacks

Problem

Given a string s and an integer k, repeatedly remove k adjacent and equal characters from s until no more such removals are possible, and return the final string.

  • 1 ≤ s.length ≤ 10⁵
  • 2 ≤ k ≤ 10⁴
  • s consists of lowercase English letters.

Example

Input: s = "deeedbbcccbdaa", k = 3
Output: "aa"

We maintain a stack of pairs (char, count). Each pair represents a compressed run of the current string after all deletions so far. Notation: stack = [(d,1),(e,2)] means the current built string is "dee". Start: stack = [] Read 'd': push (d,1) stack = [(d,1)] Read 'e': push (e,1) stack = [(d,1),(e,1)] Read 'e': increment top to (e,2) stack = [(d,1),(e,2)] Read 'e': increment top to (e,3) => count hits k, pop it stack = [(d,1)] Read 'd': increment top to (d,2) stack = [(d,2)] Read 'b': push (b,1) stack = [(d,2),(b,1)] Read 'b': increment top to (b,2) stack = [(d,2),(b,2)] Read 'c': push (c,1) stack = [(d,2),(b,2),(c,1)] Read 'c': increment top to (c,2) stack = [(d,2),(b,2),(c,2)] Read 'c': increment top to (c,3) => count hits k, pop it stack = [(d,2),(b,2)] Read 'b': increment top to (b,3) => count hits k, pop it stack = [(d,2)] Read 'd': increment top to (d,3) => count hits k, pop it stack = [] Read 'a': push (a,1) stack = [(a,1)] Read 'a': increment top to (a,2) stack = [(a,2)] Reconstruct output by expanding the stack: "a" repeated 2 times => "aa"

Approach

Straightforward Solution

A naive approach would repeatedly scan the string to find k-length groups and remove them, which could lead to O(n^2) or worse time complexity due to repeated rescanning and string rebuilding.

Core Observation

The problem requires removing groups of k identical adjacent characters repeatedly until no such groups remain. This implies a need to track consecutive characters and their counts dynamically as the string is processed.

Path to Optimal

The key insight is to process the string in a single pass using a stack that stores pairs of (character, count). For each character, if it matches the top of the stack, increment the count; otherwise, push a new pair. When the count reaches k, pop the pair from the stack, effectively removing that group. This approach avoids rescanning and achieves linear time complexity.

Optimal Approach

Use a stack to track characters and their consecutive counts. Iterate through the string once, updating counts or pushing new entries. When a count reaches k, pop the stack to remove that group. Finally, reconstruct the string from the stack by repeating each character by its count. This guarantees O(n) time and O(n) space.

Time

O(n)

Each character is processed exactly once. Stack operations (push, pop, increment) are O(1) amortized, so total time is linear in the length of the string.

Space

O(n)

In the worst case, no removals occur and the stack stores all characters with their counts, requiring space proportional to the input size.

Pattern Spotlight

Stack (Count Tracking for Adjacent Removal)

Use a stack to track consecutive character counts, and remove groups immediately when counts reach k, enabling a single-pass linear-time solution without rescanning.

Solution

1class Solution:
2 def removeDuplicates(self, s: str, k: int) -> str:
3 stack = []
4
5 for c in s:
6 if stack and stack[-1][0] == c:
7 stack[-1][1] += 1
8 else:
9 stack.append([c, 1])
10
11 if stack[-1][1] == k:
12 stack.pop()
13
14 return ''.join(c * n for c, n in stack)

Step-by-Step Solution

1

Track Consecutive Characters and Counts Using a Stack

3stack = []
5for c in s:
6 if stack and stack[-1][0] == c:
7 stack[-1][1] += 1
8 else:
9 stack.append([c, 1])

Objective

To maintain a stack that stores pairs of characters and their consecutive counts while iterating through the string.

Key Insight

By storing each character along with how many times it has appeared consecutively, the algorithm can efficiently detect when a group of size k is formed. This avoids rescanning and enables immediate removal by popping the stack. The stack acts as a compressed representation of the string's current state.

Interview Quick-Check

Core Logic

The stack stores pairs [character, count], incrementing count if the current character matches the top, or pushing a new pair otherwise.

State & Boundaries

Check if the stack is non-empty before accessing the top element to avoid errors.

Common Pitfalls & Bugs

Forgetting to handle the empty stack case before comparing characters leads to runtime errors.

2

Remove Groups When Count Reaches k by Popping the Stack

11if stack[-1][1] == k:
12 stack.pop()

Objective

To detect when a consecutive group reaches size k and remove it immediately by popping the stack.

Key Insight

When the count of the top stack element reaches k, it means a group of k identical adjacent characters has formed and should be removed. Popping the stack removes this group from the current string state. This immediate removal prevents the group from contributing to the final string and avoids the need for further rescanning or complex string manipulation.

Interview Quick-Check

Core Logic

After incrementing the count for a character, check if it equals k; if so, pop the stack to remove the group.

State & Boundaries

Ensure the count check happens after updating the count to catch newly formed groups.

Common Pitfalls & Bugs

Delaying removal or failing to pop immediately leads to incorrect final strings or extra complexity.

3

Reconstruct the Final String from the Stack

14return ''.join(c * n for c, n in stack)

Objective

To build the resulting string by repeating each character in the stack by its count.

Key Insight

After processing the entire input string and removing all k-sized groups, the stack contains the compressed form of the final string. Reconstructing the string by repeating each character by its count restores the correct order and content. This step leverages the stack's maintained state to efficiently produce the output without additional scanning or complex operations.

Interview Quick-Check

Core Logic

Use a generator expression to concatenate each character repeated by its count from the stack.

Complexity

This reconstruction is O(n) time, proportional to the length of the final string.

Line Analysis

This solution has 3 Critical lines interviewers watch for.

Line 7 Critical
stack[-1][1] += 1

Increment the count of the top stack element since the current character matches it.

This line is critical because it updates the run-length count in place, enabling immediate detection of groups of size k without rescanning or additional data structures.

Line 11 Critical
if stack[-1][1] == k:

Check if the count of the top stack element has reached k.

This check is the pivotal moment where the algorithm decides to remove a group, preventing it from appearing in the final string and enabling a linear-time solution.

Line 12 Critical
stack.pop()

Remove the top stack element to delete the group of k characters.

This removal is the core operation that enforces the problem's requirement, enabling the algorithm to maintain a compressed state and avoid costly repeated scans.

Line 3 Core
stack = []

Initialize an empty stack to store pairs of characters and their consecutive counts.

The stack is the core data structure enabling efficient tracking of consecutive characters and their counts, which is essential for detecting and removing k-sized groups in a single pass.

Line 6 Core
if stack and stack[-1][0] == c:

Check if the stack is non-empty and the current character matches the character at the top of the stack.

This condition identifies whether the current character continues a consecutive run, enabling incrementing the count rather than starting a new run.

Line 8 Core
else:

Start a new consecutive group by pushing the current character with count 1 onto the stack.

When the current character differs from the top of the stack or the stack is empty, a new group begins, so a new pair is pushed to track this fresh run.

Line 14 Core
return ''.join(c * n for c, n in stack)

Reconstruct and return the final string by repeating each character in the stack by its count.

This step translates the compressed stack representation back into the final string after all removals, ensuring the output reflects all k-sized group deletions.

Line 9 Standard
stack.append([c, 1])

Append the new character-count pair to the stack.

This action records the start of a new consecutive character run, maintaining the stack's invariant of tracking runs.

Line 5 Standard
for c in s:

Iterate through each character in the input string s.

Processing characters sequentially allows the algorithm to build and update the stack state dynamically, reflecting the current run-length encoding of the string.

Test Your Understanding

Why does using a stack with counts allow removal of k adjacent duplicates in a single pass without rescanning the string?

Because the stack maintains the current run-length counts of characters, allowing immediate detection and removal of groups of size k as soon as they form, eliminating the need to revisit previous characters.

Related Problems

Stacks pattern

Don't just read it. Drill it.

Reconstruct Remove All Adjacent Duplicates in String II from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Remove All Adjacent Duplicates in String II drill

or drill a free problem