316. Remove Duplicate Letters
Given a string which contains only lowercase letters, 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.
Example 1:
Input: “bcabc”
Output: “abc”
Example 2:
Input: “cbacdcbc”
Output: “acdb”
analysis
- The leftmost letter in our solution will be the smallest letter such that the suffix from that letter contains every other. This is because we know that the solution must have one copy of every letter, and we know that the solution will have the lexicographically smallest leftmost character possible.
If there are multiple smallest letters, then we pick the leftmost one simply because it gives us more options. We can always eliminate more letters later on, so the optimal solution will always remain in our search space.
- As we iterate over our string, if character i is greater than character i+1 and another occurrence of character i exists later in the string, deleting character i will always lead to the optimal solution. Characters that come later in the string i don’t matter in this calculation because i is in a more significant spot. Even if character i+1 isn’t the best yet, we can always replace it for a smaller character down the line if possible.
简单来说第一点是:输出结果最前面的字符是一个字符可以使得他后面包含了所有其他字符的最小的字符。
第二点是:如果一个字符大于他后面的一个字符,同时在后面还出现了至少一次,那么删除掉这个字符可以更趋近于最终输出结果
solution 1: resursion & greedy
采用第一点的思想,同时采用recursion的思路。
首先从左向右扫描找到输出结果的leftmost letter,然后recursive call从此字符开始的substring,得到下一个leftmost letter,最后得到全部结果。同时需要注意的是,leftmost有可能在后面再次出现,所以在recursive call substring时,需要将所有的当前leftmost letter替换成空字符。
public String removeDuplicateLetters(String s) {
// find pos - the index of the leftmost letter in our solution
// we create a counter and end the iteration once the suffix doesn't have each unique character
// pos will be the index of the smallest character we encounter before the iteration ends
int[] cnt = new int[26];
int pos = 0;
for (int i = 0; i < s.length(); i++) cnt[s.charAt(i) - 'a']++;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) < s.charAt(pos)) pos = i;
if (--cnt[s.charAt(i) - 'a'] == 0) break;
}
// our answer is the leftmost letter plus the recursive call on the remainder of the string
// note that we have to get rid of further occurrences of s[pos] to ensure that there are no duplicates
return s.length() == 0 ? "" : s.charAt(pos) + removeDuplicateLetters(s.substring(pos + 1).replaceAll("" + s.charAt(pos), ""));
}
solution 2 : Greedy
用一个stack来存储最后的输出结果。
首先判断当前字符是否在当前解中,如果存在可以直接跳过,否则当tack未空时,如果这个字符小于当前解的最后一个字符,且在后面当前解的最后一个字符还会出现,此时就可以删掉当前解的最后一个字符。最后在stack中加入当前字符。
采用HashSet另存了一份输出结果是为了使判断某个字符是否存在的时间复杂度降低到
O
(
n
)
O(n)
O(n)。
public String removeDuplicateLetters(String s) {
Stack<Character> stack = new Stack<>();
// this lets us keep track of what's in our solution in O(1) time
// otherwise we need to check whether a character exists in the stack, which
// takes O(n) time.
HashSet<Character> seen = new HashSet<>();
// this will let us know if there are any more instances of s[i] left in s
HashMap<Character, Integer> last_occurrence = new HashMap<>();
for(int i = 0; i < s.length(); i++) last_occurrence.put(s.charAt(i), i);
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
// we can only try to add c if it's not already in our solution
// this is to maintain only one of each character
if (!seen.contains(c)){
// if the last letter in our solution:
// 1. exists
// 2. is greater than c so removing it will make the string smaller
// 3. it's not the last occurrence
// we remove it from the solution to keep the solution optimal
while(!stack.isEmpty() && c < stack.peek() && last_occurrence.get(stack.peek()) > i){
seen.remove(stack.pop());
}
seen.add(c);
stack.push(c);
}
}
StringBuilder sb = new StringBuilder(stack.size());
for (Character c : stack) sb.append(c.charValue());
return sb.toString();
}