学算法认准 labuladong

东哥带你手把手撕力扣????

点击下方卡片即可搜索????

想啥呢?labuladong 怎么可能被整不会?只是东哥又发现了一个有趣的套路,所以写了篇文章分享给大家~

读完本文,你可以去力扣解决如下题目:

第 316 题「去除重复字母」,难度 Hard 

第 1081 题「不同字符的最小子序列」,难度 Medium

关于去重算法,应该没什么难度,往哈希集合里面塞不就行了么?

最多给你加点限制,问你怎么给有序数组原地去重,这个我们旧文 如何高效地给有序数组/链表去重 讲过。

本文讲的问题应该是去重相关算法中难度最大的了,把这个问题搞懂,就再也不用怕数组去重问题了。

这是力扣第 316 题「去除重复字母」,题目如下:

这道题和第 1081 题「不同字符的最小子序列」的解法是完全相同的,你可以把这道题的解法代码直接粘过去把 1081 题也干掉。

题目的要求总结出来有三点:

要求一、要去重

要求二、去重字符串中的字符顺序不能打乱s中字符出现的相对顺序

要求三、在所有符合上一条要求的去重字符串中,字典序最小的作为最终结果。

上述三条要求中,要求三可能有点难理解,举个例子。

比如说输入字符串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();
}

这段代码也好理解,就是插入了一个 while 循环,连续 pop 出比当前字符小的栈顶字符,直到栈顶元素比当前元素的字典序还小为止。只是不是有点「单调栈」的意思了?

这样,对于输入s = "bcabc",我们可以得出正确结果"abc"了。

但是,如果我改一下输入,假设s = "bcac",按照刚才的算法逻辑,返回的结果是"ac",而正确答案应该是"bac",分析一下这是怎么回事?

很容易发现,因为s中只有唯一一个'b',即便字符'a'的字典序比字符'b'要小,字符'b'也不应该被 pop 出去。

那问题出在哪里?

我们的算法在stk.peek() > c时才会 pop 元素,其实这时候应该分两种情况

情况一、如果stk.peek()这个字符之后还会出现,那么可以把它 pop 出去,反正后面还有嘛,后面再 push 到栈里,刚好符合字典序的要求。

情况二、如果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) {// 若之后不存在栈顶元素了,则停止 popif (count[stk.peek()] == 0) {break;}// 若之后还有,则可以 popinStack[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)。

你还记得我们开头提到的三个要求吗?我们是怎么达成这三个要求的

要求一、通过inStack这个布尔数组做到栈stk中不存在重复元素。

要求二、我们顺序遍历字符串s,通过「栈」这种顺序结构的 push/pop 操作记录结果字符串,保证了字符出现的顺序和s中出现的顺序一致。

这里也可以想到为什么要用「栈」这种数据结构,因为先进后出的结构允许我们立即操作刚插入的字符,如果用「队列」的话肯定是做不到的。

要求三、我们用类似单调栈的思路,配合计数器count不断 pop 掉不符合最小字典序的字符,保证了最终得到的结果字典序最小。

当然,由于栈的结构特点,我们最后需要把栈中元素取出后再反转一次才是最终结果。

说实话,这应该是数组去重的最高境界了,没做过还真不容易想出来。你学会了吗?学会了点个「在看」?

往期推荐 ????

二叉树的题,就那几个框架,枯燥至极????

状态压缩技巧:动态规划的降维打击

一个函数秒杀 2Sum 3Sum 4Sum 问题

回溯算法和动态规划,到底谁是谁爹

东哥手写正则通配符算法,结构清晰,包教包会!

_____________

学好算法全靠套路,认准 labuladong 就够了。

知乎、B站搜索:labuladong

《labuladong的算法小抄》即将出版,公众号后台回复关键词「pdf」下载,回复「进群」可加入刷题群。

啊这,一道数组去重的算法题把东哥整不会了…相关推荐

  1. 阶乘相关的算法题,东哥又整活儿了

    学算法认准 labuladong 东哥带你手把手撕力扣???? 点击下方卡片即可搜索???? 读完本文,你可以去 LeetCode 上拿下如下题目: 172.阶乘后的零(难度 Easy) 793.阶乘 ...

  2. java实现对英文字母去重_LeetCode算法题-Unique Morse Code Words(Java实现)

    这是悦乐书的第318次更新,第339篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第186题(顺位题号是804).国际莫尔斯电码定义了一种标准编码,其中每个字母映射到一系 ...

  3. 【算法精讲】分享一道很不错的算法题

    戳蓝字"CSDN云计算"关注我们哦! 技术头条:干货.简洁.多维全面.更多云计算精华知识尽在眼前,get要点.solve难题,统统不在话下! 作者:帅地 转自:苦逼的码农 分享一道 ...

  4. 算法精讲:分享一道值得分享的算法题

    分享一道leetcode上的题,当然,居然不是放在刷题贴里来讲,意味着分享的这道题不仅仅是教你怎么来解决,更重要的是这道题引发出来的一些解题技巧或许可以用在其他地方,下面我们来看看这道题的描述. 问题 ...

  5. 一道简约而不简单的算法题——数据流的中位数 | 附动画解析

    作者 | 程序员小吴 转载自微信公众号(ID:CXYxiaowu) 题目来源于 LeetCode 上第 295 号问题:数据流的中位数.难度级别为 Hard,目前通过率为 33.5% . 题目描述 中 ...

  6. leetcode:一道清华考研专业课算法题

    贪心算法: class Solution {public int maxSubArray(int[] nums) {int Ret = nums[0],sum =0;for(int i =0;i< ...

  7. 经典算法题:寻找数组中第k大的元素

    这算是一道相当经典的算法题了: 在长度为N的乱序数组中寻找第k(n>=k)大的元素. 扩展思考:如何处理数组中的重复元素?比如,对于数组a={1,2,2,2,3,3,3},第二大的元素应该是3还 ...

  8. JS数组去重的6种算法实现

    1.遍历数组法 最简单的去重方法,实现思路:新建一新数组,遍历传入数组,值不在新数组就加入该新数组中:注意点:判断值是否在数组的方法"indexOf"是ECMAScript5 方法 ...

  9. 使用JavaScript进行数组去重——一种高效的算法

    最近比较忙,没时间更新博客,等忙完这阵子会整理一篇使用AngularJS构建一个中型的单页面应用(SPA)的文章,尽情期待!先占个坑. 数组去重的算法有很多种,以下是一种. 思路如下: 定义一个空的对 ...

最新文章

  1. valgrind——Cachegrind分析CPU的cache命中率、丢失率,用于进行代码优化。
  2. Android 应用程序集成Google 登录及二次封装
  3. MPB:扬州大学王梦芝组-​​反刍动物瘤胃原虫的分离培养与形态学分析
  4. 人群密度估计--Spatiotemporal Modeling for Crowd Counting in Videos
  5. python是一种语言还是一个软件-Python还是一种
  6. SimpleDateFormat使用详解 转
  7. Machine Learning week 7 quiz: Unsupervised Learning
  8. Linux入门基础思维导图
  9. 4、Docker 提交运行中容器作为新的镜像
  10. javafx + jfoenix相关学习资料地址整理
  11. 网络编程(part8)--传输层服务之TCP和UDP
  12. 工程院院士李国杰科学网发文称国内 AI 研究「顶不了天,落不了地」
  13. 12.混淆——数字音频采样,混淆,反混淆,脉冲序列_2
  14. 遍历树,找出所有叶子路径
  15. OpenCV-图像处理(05、图像混合)
  16. C++读写txt文件
  17. 电脑内录软件如何录制电脑系统在线声音?
  18. 微信小程序实现文件上传
  19. python分组统计excel数据_在python中对数据进行分组并与excel进行比较
  20. ceres solver 使用

热门文章

  1. 多目标柔性车间调度丨mogv算法:以算例MK01为例
  2. 【C++】Winsock套接字编程,struct sockaddr、sockaddr_in,主机网络字节序
  3. hadoop文件目录详解
  4. android httputils更换成https请求,Android开发工具类之HttpUtils
  5. 初中英语课堂用语800句 (三)
  6. 关于AndroidTv端的小白理解
  7. GoFun最新版本怎么免押金
  8. 如何看linux是arm还是amd_七夕还是单身?必备脱单指南,看二次元人物如何追对象...
  9. 谷粒商城登录没有验证码
  10. SpringBoot相比传统Spring,有哪些优势?