Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8.16 Remove All Adjacent Duplicates In String

Source: src/main/kotlin/string/stack/RemoveAllAdjacentDuplicatesInString.kt Pattern: stack-as-builder · Core page

The Problem

Repeatedly remove adjacent equal pairs until none remain.

  • Constraints: n ≤ 10⁵.

Examples

Input:  s = "abbaca"   -> Output: "ca"   ("bb" removed -> "aaca" -> "aa" removed -> "ca")

Intuition — the stack top is the previous char; a match pops

Scan left-to-right; the stack holds the current “compressed” prefix. Each char: if it equals the top, the pair collapses (pop); else push:

s.forEach { ch ->
    when {
        stack.isNotEmpty() && stack.last() == ch -> stack.removeLast()
        else -> stack.add(ch)
    }
}
return stack.joinToString("")

Why is a single pass enough? A removal can expose a new adjacent pair — the stack’s pop-then-compare handles it automatically: after popping, the next char compares against the new top. The 8.3 stack-as-state pattern in its simplest form.

Approach 1 — Repeated string replacement (O(n²))

Loop while (true) replace adjacent pairs: correct, slow.

Approach 2 — Stack builder (the repo’s version, optimal)

class RemoveAllAdjacentDuplicatesInString {
    /**
     * @param s input string
     * @return  string after removing adjacent equal pairs
     */
    fun removeDuplicates(s: String): String {
        val stack = ArrayDeque<Char>()

        s.forEach { ch ->
            when {
                stack.isNotEmpty() && stack.last() == ch -> stack.removeLast()
                else -> stack.add(ch)
            }
        }
        return stack.joinToString("")
    }
}
import java.util.*;

public class RemoveAllAdjacentDuplicatesInString {
    /**
     * @param s input string
     * @return  string after removing adjacent equal pairs
     */
    public String removeDuplicates(String s) {
        Deque<Character> stack = new ArrayDeque<>();

        for (char c : s.toCharArray()) {
            if (!stack.isEmpty() && stack.peek() == c) stack.pop();
            else stack.push(c);
        }

        StringBuilder sb = new StringBuilder();
        for (char c : stack) sb.append(c);
        return sb.reverse().toString();
    }
}
#include <string>

class RemoveAllAdjacentDuplicatesInString {
public:
    /**
     * @param s input string
     * @return  string after removing adjacent equal pairs
     */
    std::string removeDuplicates(std::string s) {
        std::string stack;

        for (char c : s) {
            if (!stack.empty() && stack.back() == c) stack.pop_back();
            else stack.push_back(c);
        }
        return stack;
    }
};
def remove_duplicates(s: str) -> str:
    """
    @param s: input string
    @return:  string after removing adjacent equal pairs
    """
    stack = []

    for ch in s:
        if stack and stack[-1] == ch:
            stack.pop()
        else:
            stack.append(ch)

    return "".join(stack)
#![allow(unused)]
fn main() {
impl Solution {
    /// @param s input string
    /// @return  string after removing adjacent equal pairs
    pub fn remove_duplicates(s: String) -> String {
        let mut stack: Vec<char> = Vec::new();

        for ch in s.chars() {
            if let Some(&top) = stack.last() {
                if top == ch { stack.pop(); continue; }
            }
            stack.push(ch);
        }

        stack.into_iter().collect()
    }
}
}

Dry run

Input: s = "abbaca".

'a': stack [a]
'b': stack [a,b]
'b': top == b -> pop.  stack [a]
'a': top == a -> pop.  stack []        (the new pair exposed by the bb removal)
'c': stack [c]
'a': stack [c,a]

Output: "ca" ✓

The cascade: removing bb exposes aa, which the next a’s pop removes — the stack’s top-after-pop is the new neighbor, so cascades need no special handling. The StringBuilder/string stack is the output itself — no separate builder.

Complexity

Time. One pass:

$$ T(n) = O(n) $$

Space. The stack:

$$ S(n) = O(n) $$

Variants & follow-ups

  • Remove All Adjacent Duplicates II — the k-repeat generalization (counts on the stack).
  • Valid Parentheses (8.1) — the matching-stack sibling.
  • Interview follow-up: “Why does one pass handle cascades?” The stack invariant is “the current compressed prefix” — after a pop, the stack already reflects the post-removal state, so the next character compares against the correct neighbor. The cascade is free: it’s just the next pop.