讀完本文,你能夠去力扣拿下以下題目:java
316.去除重複字母git
1081.不一樣字符的最小子序列github
-----------算法
關於去重算法,應該沒什麼難度,往哈希集合裏面塞不就好了麼?數組
最多給你加點限制,問你怎麼給有序數組原地去重,這個咱們舊文 如何高效地給有序數組/鏈表去重。數據結構
本文講的問題應該是去重相關算法中難度最大的了,把這個問題搞懂,就不再用怕數組去重問題了。app
這是力扣第 316 題「去除重複字母」,題目以下:框架
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。ui
這道題和第 1081 題「不一樣字符的最小子序列」的解法是徹底相同的,你能夠把這道題的解法代碼直接粘過去把 1081 題也幹掉。code
題目的要求總結出來有三點:
要求1、要去重。
要求2、去重字符串中的字符順序不能打亂 s
中字符出現的相對順序。
要求3、在全部符合上一條要求的去重字符串中,字典序最小的做爲最終結果。
上述三條要求中,要求三可能有點難理解,舉個例子。
好比說輸入字符串 s = "babc"
,去重且符合相對位置的字符串有兩個,分別是 "bac"
和 "abc"
,可是咱們的算法得返回 "abc"
,由於它的字典序更小。
按理說,若是咱們想要有序的結果,那就得對原字符串排序對吧,可是排序後就不能保證符合 s
中字符出現順序了,這彷佛是矛盾的。
其實這裏會借鑑前文 單調棧解題框架 中講到的「單調棧」的思路,沒看過也無妨,等會你就明白了。
咱們先暫時忽略要求三,用「棧」來實現一下要求一和要求二,至於爲何用棧來實現,後面你就知道了:
String removeDuplicateLetters(String s) { // 存放去重的結果 Stack<Character> stk = new Stack<>(); // 布爾數組初始值爲 false,記錄棧中是否存在某個字符 // 輸入字符均爲 ASCII 字符,因此大小 256 夠用了 boolean[] inStack = new boolean[256]; for (char c : s.toCharArray()) { // 若是字符 c 存在棧中,直接跳過 if (inStack[c]) continue; // 若不存在,則插入棧頂並標記爲存在 stk.push(c); inStack[c] = true; } StringBuilder sb = new StringBuilder(); while (!stk.empty()) { sb.append(stk.pop()); } // 棧中元素插入順序是反的,須要 reverse 一下 return sb.reverse().toString(); }
這段代碼的邏輯很簡單吧,就是用布爾數組 inStack
記錄棧中元素,達到去重的目的,此時棧中的元素都是沒有重複的。
若是輸入 s = "bcabc"
,這個算法會返回 "bca"
,已經符合要求一和要求二了,可是題目但願要的答案是 "abc"
對吧。
那咱們想想,若是想知足要求三,保證字典序,須要作些什麼修改?
在向棧 stk
中插入字符 'a'
的這一刻,咱們的算法須要知道,字符 'a'
的字典序和以前的兩個字符 'b'
和 'c'
相比,誰大誰小?
若是當前字符 'a'
比以前的字符字典序小,就有可能須要把前面的字符 pop 出棧,讓 'a'
排在前面,對吧?
那麼,咱們先改一版代碼:
String removeDuplicateLetters(String s) { Stack<Character> stk = new Stack<>(); boolean[] inStack = new boolean[256]; for (char c : s.toCharArray()) { if (inStack[c]) continue; // 插入以前,和以前的元素比較一下大小 // 若是字典序比前面的小,pop 前面的元素 while (!stk.isEmpty() && stk.peek() > c) { // 彈出棧頂元素,並把該元素標記爲不在棧中 inStack[stk.pop()] = false; } stk.push(c); inStack[c] = true; } StringBuilder sb = new StringBuilder(); while (!stk.empty()) { sb.append(stk.pop()); } return sb.reverse().toString(); }
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
這段代碼也好理解,就是插入了一個 while 循環,連續 pop 出比當前字符小的棧頂字符,直到棧頂元素比當前元素的字典序還小爲止。只是否是有點「單調棧」的意思了?
這樣,對於輸入 s = "bcabc"
,咱們能夠得出正確結果 "abc"
了。
可是,若是我改一下輸入,假設 s = "bcac"
,按照剛纔的算法邏輯,返回的結果是 "ac"
,而正確答案應該是 "bac"
,分析一下這是怎麼回事?
很容易發現,由於 s
中只有惟一一個 'b'
,即使字符 'a'
的字典序比字符 'b'
要小,字符 'b'
也不該該被 pop 出去。
那問題出在哪裏?
咱們的算法在 stk.peek() > c
時纔會 pop 元素,其實這時候應該分兩種狀況:
狀況1、若是 stk.peek()
這個字符以後還會出現,那麼能夠把它 pop 出去,反正後面還有嘛,後面再 push 到棧裏,恰好符合字典序的要求。
狀況2、若是 stk.peek()
這個字符以後不會出現了,前面也說了棧中不會存在重複的元素,那麼就不能把它 pop 出去,不然你就永遠失去了這個字符。
回到 s = "bcac"
的例子,插入字符 'a'
的時候,發現前面的字符 'c'
的字典序比 'a'
大,且在 'a'
以後還存在字符 'c'
,那麼棧頂的這個 'c'
就會被 pop 掉。
while 循環繼續判斷,發現前面的字符 'b'
的字典序仍是比 'a'
大,可是在 'a'
以後再沒有字符 'b'
了,因此不該該把 'b'
pop 出去。
那麼關鍵就在於,如何讓算法知道字符 'a'
以後有幾個 'b'
有幾個 'c'
呢?
也不難,只要再改一版代碼:
String removeDuplicateLetters(String s) { Stack<Character> stk = new Stack<>(); // 維護一個計數器記錄字符串中字符的數量 // 由於輸入爲 ASCII 字符,大小 256 夠用了 int[] count = new int[256]; for (int i = 0; i < s.length(); i++) { count[s.charAt(i)]++; } boolean[] inStack = new boolean[256]; for (char c : s.toCharArray()) { // 每遍歷過一個字符,都將對應的計數減一 count[c]--; if (inStack[c]) continue; while (!stk.isEmpty() && stk.peek() > c) { // 若以後不存在棧頂元素了,則中止 pop if (count[stk.peek()] == 0) { break; } // 若以後還有,則能夠 pop inStack[stk.pop()] = false; } stk.push(c); inStack[c] = true; } StringBuilder sb = new StringBuilder(); while (!stk.empty()) { sb.append(stk.pop()); } return sb.reverse().toString(); }
咱們用了一個計數器 count
,當字典序較小的字符試圖「擠掉」棧頂元素的時候,在 count
中檢查棧頂元素是不是惟一的,只有當後面還存在棧頂元素的時候才能擠掉,不然不能擠掉。
至此,這個算法就結束了,時間空間複雜度都是 O(N)。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
你還記得咱們開頭提到的三個要求嗎?咱們是怎麼達成這三個要求的?
要求1、經過 inStack
這個布爾數組作到棧 stk
中不存在重複元素。
要求2、咱們順序遍歷字符串 s
,經過「棧」這種順序結構的 push/pop 操做記錄結果字符串,保證了字符出現的順序和 s
中出現的順序一致。
這裏也能夠想到爲何要用「棧」這種數據結構,由於先進後出的結構容許咱們當即操做剛插入的字符,若是用「隊列」的話確定是作不到的。
要求3、咱們用相似單調棧的思路,配合計數器 count
不斷 pop 掉不符合最小字典序的字符,保證了最終獲得的結果字典序最小。
固然,因爲棧的結構特色,咱們最後須要把棧中元素取出後再反轉一次纔是最終結果。
說實話,這應該是數組去重的最高境界了,沒作過還真不容易想出來。你學會了嗎?
_____________
個人 在線電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 算法倉庫 已經得到了 70k star,歡迎標星!