You are given a string s and an integer k, a kduplicate removal consists of choosing k adjacent and equal letters from s and removing them, causing the left and the right side of the deleted substring to concatenate together.
We repeatedly make kduplicate removals on s until we no longer can.
Return the final string after all such duplicate removals have been made. It is guaranteed that the answer is unique.
Example 1:
Input: s ="abcd", k =2Output:"abcd"
Explanation: There's nothing to delete.
Example 2:
Input: s ="deeedbbcccbdaa", k =3Output:"aa"
Explanation: First delete "eee" and "ccc", get "ddbbbdaa" Then delete "bbb", get "dddaa" Finally delete "ddd", get "aa"
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Stack Data Structure - Tracking consecutive character counts and efficiently handling removals
Two Pointers - Using read and write pointers to modify strings in-place
String Manipulation - Working with mutable character arrays and substring operations
1. Brute Force
Intuition
The most direct approach is to repeatedly scan the string looking for k consecutive identical characters. When found, we remove them and restart the scan from the beginning since the removal might create new groups of k adjacent duplicates.
This process continues until a full scan completes without finding any group to remove. While straightforward, this approach is inefficient because each removal requires rescanning from the start.
Algorithm
Convert the string to a mutable format (list or character array).
Repeatedly scan the string:
Track the current character and count consecutive occurrences.
When the count reaches k, remove those characters from the string, set a flag, and restart the scan.
If a full scan completes without any removal, exit the loop.
classSolution:defremoveDuplicates(self, s:str, k:int)->str:while s:
flag =False
cur = s[0]
cnt =1for i inrange(1,len(s)):if cur != s[i]:
cnt =0
cur = s[i]
cnt +=1if cnt == k:
s = s[:i - cnt +1]+ s[i +1:]
flag =Truebreakifnot flag:breakreturn s
publicclassSolution{publicStringremoveDuplicates(String s,int k){while(s.length()!=0){boolean flag =false;char cur = s.charAt(0);int cnt =1;for(int i =1; i < s.length(); i++){if(cur != s.charAt(i)){
cnt =0;
cur = s.charAt(i);}
cnt++;if(cnt == k){
s = s.substring(0, i - cnt +1)+ s.substring(i +1);
flag =true;break;}}if(!flag){break;}}return s;}}
classSolution{public:
string removeDuplicates(string s,int k){while(s.length()){bool flag =false;char cur = s[0];int cnt =1;for(int i =1; i < s.size(); i++){if(cur != s[i]){
cnt =0;
cur = s[i];}
cnt++;if(cnt == k){
s = s.substr(0, i - cnt +1)+ s.substr(i +1);
flag =true;break;}}if(!flag){break;}}return s;}};
classSolution{/**
* @param {string} s
* @param {number} k
* @return {string}
*/removeDuplicates(s, k){while(s){let flag =false;let cur = s[0];let cnt =1;for(let i =1; i < s.length; i++){if(cur !== s[i]){
cnt =0;
cur = s[i];}
cnt++;if(cnt === k){
s = s.slice(0, i - cnt +1)+ s.slice(i +1);
flag =true;break;}}if(!flag){break;}}return s;}}
publicclassSolution{publicstringRemoveDuplicates(string s,int k){while(s.Length !=0){bool flag =false;char cur = s[0];int cnt =1;for(int i =1; i < s.Length; i++){if(cur != s[i]){
cnt =0;
cur = s[i];}
cnt++;if(cnt == k){
s = s.Substring(0, i - cnt +1)+ s.Substring(i +1);
flag =true;break;}}if(!flag){break;}}return s;}}
funcremoveDuplicates(s string, k int)string{
bytes :=[]byte(s)forlen(bytes)>0{
flag :=false
cur := bytes[0]
cnt :=1for i :=1; i <len(bytes); i++{if cur != bytes[i]{
cnt =0
cur = bytes[i]}
cnt++if cnt == k {
bytes =append(bytes[:i-cnt+1], bytes[i+1:]...)
flag =truebreak}}if!flag {break}}returnstring(bytes)}
class Solution {funremoveDuplicates(s: String, k: Int): String {var str =StringBuilder(s)while(str.isNotEmpty()){var flag =falsevar cur = str[0]var cnt =1for(i in1 until str.length){if(cur != str[i]){
cnt =0
cur = str[i]}
cnt++if(cnt == k){
str.delete(i - cnt +1, i +1)
flag =truebreak}}if(!flag){break}}return str.toString()}}
classSolution{funcremoveDuplicates(_ s:String,_ k:Int)->String{var arr =Array(s)while!arr.isEmpty {var flag =falsevar cur = arr[0]var cnt =1for i in1..<arr.count {if cur != arr[i]{
cnt =0
cur = arr[i]}
cnt +=1if cnt == k {
arr.removeSubrange((i - cnt +1)...i)
flag =truebreak}}if!flag {break}}returnString(arr)}}
Time & Space Complexity
Time complexity: O(kn2)
Space complexity: O(n)
2. Stack
Intuition
To avoid rescanning the entire string after each removal, we can use a stack to track consecutive counts as we process each character. When we encounter a character, we check if it matches the previous one. If so, we increment the count; otherwise, we start a new count.
When the count reaches k, we remove those k characters and continue from where we left off. The stack helps us remember the count before the removal so we can correctly continue counting if the characters before and after the removed segment match.
Algorithm
Convert the string to a mutable list and initialize an empty stack to track counts.
Iterate through the string with an index i:
If the current character differs from the previous, push 1 onto the stack.
If it matches, increment the top of the stack.
If the count reaches k, pop from the stack, delete those k characters, and adjust the index.
classSolution:defremoveDuplicates(self, s:str, k:int)->str:
stack =[]
n =len(s)
i =0
s =list(s)while i < n:if i ==0or s[i]!= s[i -1]:
stack.append(1)else:
stack[-1]+=1if stack[-1]== k:
stack.pop()del s[i - k +1:i +1]
i -= k
n -= k
i +=1return''.join(s)
publicclassSolution{publicStringremoveDuplicates(String s,int k){StringBuilder sb =newStringBuilder(s);Stack<Integer> stack =newStack<>();int i =0;while(i < sb.length()){if(i ==0|| sb.charAt(i)!= sb.charAt(i -1)){
stack.push(1);}else{
stack.push(stack.pop()+1);if(stack.peek()== k){
stack.pop();
sb.delete(i - k +1, i +1);
i -= k;}}
i++;}return sb.toString();}}
classSolution{public:
string removeDuplicates(string s,int k){
vector<int> stack;int n = s.length(), i =0;while(i < n){if(i ==0|| s[i]!= s[i -1]){
stack.push_back(1);}else{
stack.back()++;if(stack.back()== k){
stack.pop_back();
s.erase(i - k +1, k);
i -= k;
n -= k;}}
i++;}return s;}};
classSolution{/**
* @param {string} s
* @param {number} k
* @return {string}
*/removeDuplicates(s, k){let stack =[];let arr = s.split('');let i =0;while(i < arr.length){if(i ===0|| arr[i]!== arr[i -1]){
stack.push(1);}else{
stack[stack.length -1]++;if(stack[stack.length -1]=== k){
stack.pop();
arr.splice(i - k +1, k);
i -= k;}}
i++;}return arr.join('');}}
publicclassSolution{publicstringRemoveDuplicates(string s,int k){var stack =newList<int>();var chars =newList<char>(s);int n = chars.Count;int i =0;while(i < n){if(i ==0|| chars[i]!= chars[i -1]){
stack.Add(1);}else{
stack[stack.Count -1]++;if(stack[stack.Count -1]== k){
stack.RemoveAt(stack.Count -1);
chars.RemoveRange(i - k +1, k);
i -= k;
n -= k;}}
i++;}returnnewstring(chars.ToArray());}}
funcremoveDuplicates(s string, k int)string{
stack :=[]int{}
arr :=[]byte(s)
n :=len(arr)
i :=0for i < n {if i ==0|| arr[i]!= arr[i-1]{
stack =append(stack,1)}else{
stack[len(stack)-1]++if stack[len(stack)-1]== k {
stack = stack[:len(stack)-1]
arr =append(arr[:i-k+1], arr[i+1:]...)
i -= k
n -= k
}}
i++}returnstring(arr)}
class Solution {funremoveDuplicates(s: String, k: Int): String {val stack = mutableListOf<Int>()val arr =StringBuilder(s)var n = arr.length
var i =0while(i < n){if(i ==0|| arr[i]!= arr[i -1]){
stack.add(1)}else{
stack[stack.size -1]++if(stack[stack.size -1]== k){
stack.removeAt(stack.size -1)
arr.delete(i - k +1, i +1)
i -= k
n -= k
}}
i++}return arr.toString()}}
classSolution{funcremoveDuplicates(_ s:String,_ k:Int)->String{var stack =[Int]()var arr =Array(s)var n = arr.count
var i =0while i < n {if i ==0|| arr[i]!= arr[i -1]{
stack.append(1)}else{
stack[stack.count -1]+=1if stack[stack.count -1]== k {
stack.removeLast()
arr.removeSubrange((i - k +1)...i)
i -= k
n -= k
}}
i +=1}returnString(arr)}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
3. Stack (Optimal)
Intuition
Instead of modifying the string and tracking counts separately, we can store both the character and its count together in the stack. This eliminates the need for in-place string manipulation and index adjustments.
Each stack entry is a pair of (character, count). When we encounter a new character, we either increment the count of the top entry (if it matches) or push a new entry. When a count reaches k, we simply pop that entry. Building the result at the end involves expanding each entry back into its characters.
Algorithm
Initialize a stack where each entry stores a character and its consecutive count.
For each character in the string:
If the stack is non-empty and the top character matches, increment its count.
Otherwise, push a new entry with count 1.
If the count reaches k, pop the entry from the stack.
Build the result by expanding each stack entry: repeat each character by its count.
funcremoveDuplicates(s string, k int)string{
stack :=[][]int{}// [char, count]for_, c :=range s {iflen(stack)>0&& stack[len(stack)-1][0]==int(c){
stack[len(stack)-1][1]++}else{
stack =append(stack,[]int{int(c),1})}if stack[len(stack)-1][1]== k {
stack = stack[:len(stack)-1]}}var res strings.Builder
for_, item :=range stack {for i :=0; i < item[1]; i++{
res.WriteByte(byte(item[0]))}}return res.String()}
class Solution {funremoveDuplicates(s: String, k: Int): String {val stack = mutableListOf<Pair<Char, Int>>()for(c in s){if(stack.isNotEmpty()&& stack.last().first == c){
stack[stack.size -1]= c to stack.last().second +1}else{
stack.add(c to1)}if(stack.last().second == k){
stack.removeLast()}}val res =StringBuilder()for((ch, cnt)in stack){
res.append(ch.toString().repeat(cnt))}return res.toString()}}
classSolution{funcremoveDuplicates(_ s:String,_ k:Int)->String{var stack =[(Character,Int)]()for c in s {if!stack.isEmpty && stack.last!.0== c {
stack[stack.count -1].1+=1}else{
stack.append((c,1))}if stack.last!.1== k {
stack.removeLast()}}var res =""for(ch, cnt)in stack {
res +=String(repeating:String(ch), count: cnt)}return res
}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
4. Two Pointers
Intuition
This approach uses the input array itself as both the working space and the result, avoiding the need for a separate stack data structure. We use two pointers: j reads through the original string while i writes the result.
A separate count array tracks consecutive occurrences at each write position. When we write a character, we check if it matches the previous written character to determine its count. If the count reaches k, we "rewind" the write pointer by k positions, effectively removing those characters.
Algorithm
Convert the string to a mutable character array and create a count array of the same size.
Use two pointers: j iterates through the original string, i tracks the write position.
For each character at position j:
Copy it to position i.
Set its count to 1. If the previous character (at i-1) is the same, add the previous count to the current count.
If the count reaches k, move i back by k positions.
classSolution:defremoveDuplicates(self, s:str, k:int)->str:
i =0
n =len(s)
count =[0]* n
s =list(s)for j inrange(n):
s[i]= s[j]
count[i]=1if i >0and s[i -1]== s[j]:
count[i]+= count[i -1]if count[i]== k:
i -= k
i +=1return''.join(s[:i])
funcremoveDuplicates(s string, k int)string{
n :=len(s)
arr :=[]byte(s)
count :=make([]int, n)
i :=0for j :=0; j < n; j++{
arr[i]= arr[j]
count[i]=1if i >0&& arr[i-1]== arr[j]{
count[i]+= count[i-1]}if count[i]== k {
i -= k
}
i++}returnstring(arr[:i])}
class Solution {funremoveDuplicates(s: String, k: Int): String {val n = s.length
val arr = s.toCharArray()val count =IntArray(n)var i =0for(j in0 until n){
arr[i]= arr[j]
count[i]=1if(i >0&& arr[i -1]== arr[j]){
count[i]+= count[i -1]}if(count[i]== k){
i -= k
}
i++}returnString(arr,0, i)}}
classSolution{funcremoveDuplicates(_ s:String,_ k:Int)->String{let n = s.count
var arr =Array(s)var count =[Int](repeating:0, count: n)var i =0for j in0..<n {
arr[i]= arr[j]
count[i]=1if i >0&& arr[i -1]== arr[j]{
count[i]+= count[i -1]}if count[i]== k {
i -= k
}
i +=1}returnString(arr.prefix(i))}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
Common Pitfalls
Not Continuing After Removal Creates New Groups
After removing k adjacent duplicates, the characters before and after the removed section may now be adjacent and form a new group of duplicates. A common mistake is stopping after a single removal pass. The solution must continue checking for new groups that may have been created by previous removals.
Resetting Count When Characters Match After Removal
When using a stack-based approach, after removing k characters, the new top of the string may match the next character being processed. Failing to properly inherit or restore the count from before the removal leads to incorrect duplicate detection. The count must reflect the consecutive sequence that existed before the removal.
Off-by-One Errors in Substring Removal
When removing k characters ending at index i, the removal range is from i - k + 1 to i (inclusive). A common bug is using i - k as the start index or forgetting to adjust the current index after removal. Both the string length and the iteration index must be updated correctly to avoid processing removed characters or skipping valid ones.