diff --git a/大厂刷题班/class24/Code06_RemoveDuplicateLettersLessLexi.java b/大厂刷题班/class24/Code06_RemoveDuplicateLettersLessLexi.java index 2672411..c22d046 100644 --- a/大厂刷题班/class24/Code06_RemoveDuplicateLettersLessLexi.java +++ b/大厂刷题班/class24/Code06_RemoveDuplicateLettersLessLexi.java @@ -1,74 +1,61 @@ package class24; +// 本题重写了 +// 不用看课上的解释了 +// 这道题最优解是单调栈的解法 +// 看注释能看懂 // 本题测试链接 : https://leetcode.com/problems/remove-duplicate-letters/ public class Code06_RemoveDuplicateLettersLessLexi { - // 在str中,每种字符都要保留一个,让最后的结果,字典序最小 ,并返回 - public static String removeDuplicateLetters1(String str) { - if (str == null || str.length() < 2) { - return str; + public static String removeDuplicateLetters(String s) { + int[] cnts = new int[26]; + boolean[] enter = new boolean[26]; + for (int i = 0; i < s.length(); i++) { + cnts[s.charAt(i) - 'a']++; } - int[] map = new int[256]; - for (int i = 0; i < str.length(); i++) { - map[str.charAt(i)]++; - } - int minACSIndex = 0; - for (int i = 0; i < str.length(); i++) { - minACSIndex = str.charAt(minACSIndex) > str.charAt(i) ? i : minACSIndex; - if (--map[str.charAt(i)] == 0) { - break; - } - } - // 0...break(之前) minACSIndex - // str[minACSIndex] 剩下的字符串str[minACSIndex+1...] -> 去掉str[minACSIndex]字符 -> s' - // s'... - return String.valueOf(str.charAt(minACSIndex)) + removeDuplicateLetters1( - str.substring(minACSIndex + 1).replaceAll(String.valueOf(str.charAt(minACSIndex)), "")); - } - - public static String removeDuplicateLetters2(String s) { - char[] str = s.toCharArray(); - // 小写字母ascii码值范围[97~122],所以用长度为26的数组做次数统计 - // 如果map[i] > -1,则代表ascii码值为i的字符的出现次数 - // 如果map[i] == -1,则代表ascii码值为i的字符不再考虑 - int[] map = new int[26]; - for (int i = 0; i < str.length; i++) { - map[str[i] - 'a']++; - } - char[] res = new char[26]; - int index = 0; - int L = 0; - int R = 0; - while (R != str.length) { - // 如果当前字符是不再考虑的,直接跳过 - // 如果当前字符的出现次数减1之后,后面还能出现,直接跳过 - if (map[str[R] - 'a'] == -1 || --map[str[R] - 'a'] > 0) { - R++; - } else { // 当前字符需要考虑并且之后不会再出现了 - // 在str[L..R]上所有需要考虑的字符中,找到ascii码最小字符的位置 - int pick = -1; - for (int i = L; i <= R; i++) { - if (map[str[i] - 'a'] != -1 && (pick == -1 || str[i] < str[pick])) { - pick = i; - } - } - // 把ascii码最小的字符放到挑选结果中 - res[index++] = str[pick]; - // 在上一个的for循环中,str[L..R]范围上每种字符的出现次数都减少了 - // 需要把str[pick + 1..R]上每种字符的出现次数加回来 - for (int i = pick + 1; i <= R; i++) { - if (map[str[i] - 'a'] != -1) { // 只增加以后需要考虑字符的次数 - map[str[i] - 'a']++; - } + // 单调栈 + // 从左到右只保留依次变大的字符 + // 比如: a c e.... + char[] stack = new char[26]; + int size = 0; + for (int i = 0; i < s.length(); i++) { + // 假设当前字符是d + char cur = s.charAt(i); + // 如果d已经在单调栈里,不进入! + // 因为单调栈里每种字符只保留一个 + if (!enter[cur - 'a']) { + // 如果d不在单调栈里,进入! + enter[cur - 'a'] = true; + // 如果单调栈里已经有 : + // a c e f + // 当前字符是d + // 那么f弹出、e弹出、然后d进入, + // 单调栈变成 : a c d + // 但是! + // 如果后面还有f,f才能弹出 + // 如果后面还有e,e才能弹出 + // 如果一个字符要弹出,但是后面已经没有这种字符了,不能弹出! + // 因为一旦弹出,再也没有机会收集到这种字符了!因为后面没有了 + // 这就是核心逻辑 + // 所以 : size > 0,表示单调栈里有字符 + // stack[size - 1] > cur,表示单调栈当前最右的字符,比当前字符大,那么它弹出 + // cnts[stack[size - 1] - 'a'] > 0,重要限制 : 后面还有单调栈当前最右的字符,才能弹出 + // 缺一不可 + while (size > 0 && stack[size - 1] > cur && cnts[stack[size - 1] - 'a'] > 0) { + // 这种字符要弹出了,所以标记这种字符出去了 + enter[stack[size - 1] - 'a'] = false; + // 单调栈大小缩减了,也就是弹出了 + size--; } - // 选出的ascii码最小的字符,以后不再考虑了 - map[str[pick] - 'a'] = -1; - // 继续在str[pick + 1......]上重复这个过程 - L = pick + 1; - R = L; + // 当前字符进入了单调栈 + stack[size++] = cur; } + // 当前字符的词频调整,调整后表示后面还有多少个当前字符 + // 注意词频表更新 + cnts[cur - 'a']--; } - return String.valueOf(res, 0, index); + // 单调栈里的字符,拼字符串返回 + return String.valueOf(stack, 0, size); } }