[Medium] 316. Remove Duplicate Letters
[Medium] 316. Remove Duplicate Letters
Given a string s, remove duplicate letters so that every letter appears once and only once. You must make sure your result is the smallest in lexicographical order among all possible results.
Examples
Example 1:
Input: s = "bcabc"
Output: "abc"
Explanation:
- Remove duplicate 'b' and 'c'
- Result "abc" is lexicographically smallest
Example 2:
Input: s = "cbacdcbc"
Output: "acdb"
Explanation:
- Remove duplicate 'c' and 'b'
- Result "acdb" is lexicographically smallest
Example 3:
Input: s = "bbcaac"
Output: "bac"
Explanation:
- Remove duplicate 'b' and 'c'
- Result "bac" is lexicographically smallest
Constraints
1 <= s.length <= 10^4sconsists of lowercase English letters only.
Solution: Monotonic Stack with Greedy Approach
Time Complexity: O(n) where n is the length of string
Space Complexity: O(1) since we use at most 26 characters
Use a monotonic stack to maintain lexicographically smallest result while ensuring each character appears exactly once.
class Solution {
public:
string removeDuplicateLetters(string s) {
vector<int> count(26, 0);
vector<bool> visited(26, false);
stack<char> st;
// Count frequency of each character
for(char c : s) {
count[c - 'a']++;
}
for(char c : s) {
count[c - 'a']--;
// Skip if already in result
if(visited[c - 'a']) continue;
// Remove characters that are:
// 1. Greater than current character
// 2. Will appear again later
while(!st.empty() && st.top() > c && count[st.top() - 'a'] > 0) {
visited[st.top() - 'a'] = false;
st.pop();
}
st.push(c);
visited[c - 'a'] = true;
}
string result;
while(!st.empty()) {
result = st.top() + result;
st.pop();
}
return result;
}
};
How the Algorithm Works
Key Insight: Use a monotonic stack to maintain lexicographically smallest result while ensuring each character appears exactly once.
Steps:
- Count frequency of each character in the string
- Use visited array to track characters already in result
- For each character:
- Skip if already processed
- Remove characters from stack that are:
- Greater than current character (lexicographically)
- Will appear again later (count > 0)
- Add current character to stack and mark as visited
- Build result from stack
Step-by-Step Example
Example: s = "cbacdcbc"
Initial state:
count = [2,2,2,1](a=2, b=2, c=2, d=1)visited = [false,false,false,false]stack = []
Processing each character:
| Char | Count After | Visited | Stack Operation | Stack State |
|---|---|---|---|---|
| ‘c’ | [2,2,1,1] | [f,f,t,f] | Push ‘c’ | [‘c’] |
| ‘b’ | [2,1,1,1] | [f,t,t,f] | Push ‘b’ | [‘c’,’b’] |
| ‘a’ | [1,1,1,1] | [t,t,t,f] | Pop ‘b’,’c’, Push ‘a’ | [‘a’] |
| ‘c’ | [1,1,0,1] | [t,t,t,f] | Skip (already visited) | [‘a’] |
| ‘d’ | [1,1,0,0] | [t,t,t,t] | Push ‘d’ | [‘a’,’d’] |
| ‘c’ | [1,1,0,0] | [t,t,t,t] | Skip (already visited) | [‘a’,’d’] |
| ‘b’ | [1,0,0,0] | [t,t,t,t] | Skip (already visited) | [‘a’,’d’] |
| ‘c’ | [0,0,0,0] | [t,t,t,t] | Skip (already visited) | [‘a’,’d’] |
Final result: "acdb"
Algorithm Breakdown
Core Logic:
for(char c : s) {
count[c - 'a']--;
// Skip if already in result
if(visited[c - 'a']) continue;
// Remove characters that are:
// 1. Greater than current character
// 2. Will appear again later
while(!st.empty() && st.top() > c && count[st.top() - 'a'] > 0) {
visited[st.top() - 'a'] = false;
st.pop();
}
st.push(c);
visited[c - 'a'] = true;
}
Process:
- Decrement count for current character
- Skip if already processed (visited)
- Remove larger characters that will appear again
- Add current character to stack
- Mark as visited
Complexity Analysis
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Count frequency | O(n) | O(1) |
| Process characters | O(n) | O(1) |
| Stack operations | O(n) | O(1) |
| Build result | O(n) | O(1) |
| Total | O(n) | O(1) |
Where n is the length of the string.
Edge Cases
- Single character:
s = "a"→"a" - All same characters:
s = "aaaa"→"a" - Already sorted:
s = "abc"→"abc" - Reverse sorted:
s = "cba"→"abc"
Key Insights
Greedy Strategy:
- Lexicographically smallest: Always prefer smaller characters
- One occurrence: Each character appears exactly once
- Future availability: Consider if character will appear again
- Stack property: Maintains order and allows efficient removal
Monotonic Stack:
- Maintains order: Characters in lexicographical order
- Efficient removal: Can remove multiple characters at once
- Future consideration: Checks if characters will appear again
- Optimal result: Ensures smallest lexicographical order
Detailed Example Walkthrough
Example: s = "bcabc"
Initial state:
count = [1,2,2](a=1, b=2, c=2)visited = [false,false,false]stack = []
Processing each character:
| Char | Count After | Visited | Stack Operation | Stack State | Explanation |
|---|---|---|---|---|---|
| ‘b’ | [1,1,2] | [f,t,f] | Push ‘b’ | [‘b’] | First ‘b’, add to stack |
| ‘c’ | [1,1,1] | [f,t,t] | Push ‘c’ | [‘b’,’c’] | First ‘c’, add to stack |
| ‘a’ | [0,1,1] | [t,t,t] | Pop ‘c’,’b’, Push ‘a’ | [‘a’] | ‘a’ < ‘c’,’b’, and ‘c’,’b’ will appear again |
| ‘b’ | [0,0,1] | [t,t,t] | Skip (already visited) | [‘a’] | ‘b’ already in result |
| ‘c’ | [0,0,0] | [t,t,t] | Skip (already visited) | [‘a’] | ‘c’ already in result |
Final result: "abc"
Alternative Approaches
Approach 1: Recursive with Backtracking
class Solution {
public:
string removeDuplicateLetters(string s) {
if(s.empty()) return "";
vector<int> count(26, 0);
for(char c : s) count[c - 'a']++;
int pos = 0;
for(int i = 0; i < s.length(); i++) {
if(s[i] < s[pos]) pos = i;
if(--count[s[i] - 'a'] == 0) break;
}
char c = s[pos];
string remaining = s.substr(pos + 1);
for(char& ch : remaining) {
if(ch == c) ch = ' ';
}
return c + removeDuplicateLetters(remaining);
}
};
Time Complexity: O(n^2)
Space Complexity: O(n)
Approach 2: Set-based Approach
class Solution {
public:
string removeDuplicateLetters(string s) {
set<char> seen;
string result;
for(char c : s) {
if(seen.find(c) == seen.end()) {
seen.insert(c);
result += c;
}
}
sort(result.begin(), result.end());
return result;
}
};
Time Complexity: O(n log n)
Space Complexity: O(1)
Common Mistakes
- Wrong removal condition: Not checking if character will appear again
- Missing visited check: Processing same character multiple times
- Incorrect stack order: Not maintaining lexicographical order
- Count management: Not properly decrementing counts
Related Problems
- 402. Remove K Digits
- 321. Create Maximum Number
- 1081. Smallest Subsequence of Distinct Characters
- 316. Remove Duplicate Letters
Why This Solution Works
Greedy Strategy:
- Lexicographically smallest: Always prefer smaller characters
- One occurrence: Each character appears exactly once
- Future availability: Consider if character will appear again
- Optimal choice: Make locally optimal choice at each step
Monotonic Stack:
- Maintains order: Characters in lexicographical order
- Efficient removal: Can remove multiple characters at once
- Future consideration: Checks if characters will appear again
- Optimal result: Ensures smallest lexicographical order
Key Algorithm Properties:
- Correctness: Always produces valid result
- Optimality: Produces lexicographically smallest result
- Efficiency: O(n) time complexity
- Simplicity: Easy to understand and implement