贪心与单调栈的艺术:从三道 LeetCode 题看最小字典序问题(316/402/1081)

news2025/4/22 2:55:59

前言

欢迎来到我的算法探索博客,在这里,我将通过解析精选的LeetCode题目,与您分享深刻的解题思路、多元化的解决方案以及宝贵的实战经验,旨在帮助每一位读者提升编程技能,领略算法之美。
👉更多高频有趣LeetCode算法题

在字符串处理的众多问题中,“最小字典序”是一类典型的优化问题,要求我们在一系列操作约束下构造出字典序最小的字符串。LeetCode 316(去除重复字母)、402(移掉 K 位数字)、1081(不同字符的最小子序列)这三道题,尽管表面上考察的内容不同,但都围绕着同一个核心目标:如何在给定条件下,使最终结果的字典序尽可能小。

解决这类问题的关键在于 贪心策略 + 单调栈。贪心策略的核心思想是每一步都选择当前最优解,确保最终答案的整体最优;单调栈则用于维护一个满足字典序要求的递增结构,同时保证结果的合法性,例如去重或限制字符个数。通过合理地运用这两种技巧,我们可以在遍历过程中动态调整答案,确保最终构造出的字符串是全局最优的。

这一策略不仅适用于字符串去重(如 316、1081),还可以用于数字最小化(如 402),甚至在更广泛的子序列优化问题中也有所应用。本文将通过对这三道题的详细解析,帮助你掌握 贪心 + 单调栈 在最小字典序问题中的应用,让你在面对类似问题时能够迅速找到最佳解法。

402. 移掉 K 位数字316. 去除重复字母
1081. 不同字符的最小子序列/

实战:经典例题讲解

402. 移掉 K 位数字

🪴题目描述

在这里插入图片描述

🍁核心思路

1. 贪心策略 + 单调栈
  • 核心思想:要让最终的数值最小,高位数字尽可能小是关键。因此,我们需要从左到右遍历字符串,在允许删除 k 次的前提下,尽可能让高位数字更小。
  • 单调栈的作用:使用 StringBuilder 模拟一个单调非递减栈。遍历过程中,如果当前字符比栈顶字符小,且还有删除次数(k > 0),就不断弹出栈顶字符(删除高位较大的数字),直到栈顶字符不大于当前字符。

2. 遍历字符串,动态调整栈
  • 具体步骤
    1. 遍历每个字符时,检查栈顶字符是否比当前字符大:
      • 如果是,则弹出栈顶字符(相当于删除一个高位较大的数字),并减少 k
      • 重复这一过程,直到栈顶字符不大于当前字符,或者 k 用完。
    2. 将当前字符加入栈中。
  • 示例
    输入 num = "1432219", k = 3
    • 遍历到 '4' 时,栈为 ['1']'1' < '4',直接加入 → ['1', '4']
    • 遍历到 '3' 时,发现 '4' > '3',删除 '4'k=2),栈变为 ['1'],加入 '3'['1', '3']
    • 遍历到 '2' 时,发现 '3' > '2',删除 '3'k=1),栈变为 ['1'],加入 '2'['1', '2']
    • 最终结果为 "12219",删除剩余 k=1 次末尾字符 → "1219"

3. 处理剩余的删除次数
  • 如果遍历完字符串后,k 仍未用完(例如原字符串已经是单调递增的),则直接从栈末尾删除剩余的 k 位,因为末尾的数字是最大的,删除它们对数值影响最小。
    • 示例:num = "12345", k = 2 → 删除末尾两位 '4', '5',结果为 "123"

4. 处理前导零
  • 删除所有前导零(例如 "0200""200")。
  • 如果结果为空或全为零,返回 "0"

🌏代码实现

Java
class Solution {
    public String removeKdigits(String num, int k) {
        StringBuilder sb = new StringBuilder();
        for (char a : num.toCharArray()) {
            while (k > 0 && sb.length() > 0 && sb.charAt(sb.length() - 1) > a) {
                k--;
                sb.deleteCharAt(sb.length() - 1);
            }
            sb.append(a);
        }

        // 如果 k 仍然大于 0,需要继续删除最后的 k 个字符
        while (k > 0 && sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
            k--;
        }

        // 移除前导零
        int start = 0;
        for (int i = 0; i < sb.length(); i++) {
            if (sb.charAt(i) == '0')
                start++;
            else
                break;
        }

        // 如果 sb 为空或全是 0,则返回 "0"
        String result = sb.substring(start);
        return result.isEmpty() ? "0" : result;
    }
}

Python
class Solution:
    def removeKdigits(self, num, k):
        result = []
        for c in num:
            while k > 0 and result and result[-1] > c:
                result.pop()
                k -= 1
            result.append(c)

        # 如果 k 仍然大于 0,删除最后的 k 个字符
        result = result[:-k] if k else result

        # 移除前导零
        finalResult = "".join(result).lstrip('0')
        return finalResult if finalResult else "0"

C++
class Solution {
public:
    string removeKdigits(string num, int k) {
        string result;
        for (char c : num) {
            while (k > 0 && !result.empty() && result.back() > c) {
                result.pop_back();
                k--;
            }
            result.push_back(c);
        }

        // 如果 k 仍然大于 0,删除最后的 k 个字符
        while (k > 0 && !result.empty()) {
            result.pop_back();
            k--;
        }

        // 移除前导零
        int start = 0;
        while (start < result.size() && result[start] == '0') {
            start++;
        }

        // 构造最终字符串
        string finalResult = result.substr(start);
        return finalResult.empty() ? "0" : finalResult;
    }
};

316. 去除重复字母

1081. 不同字符的最小子序列

🪴题目描述

这俩题基本上一致,换个方法名就行。

在这里插入图片描述

🍁核心思路(针对Java)

1. 统计字符出现次数
  • 首先,我们需要知道每个字符在字符串中出现的次数,以便后续判断是否可以移除某个字符(如果后面还有相同的字符,就可以放心移除当前字符)。
  • 使用一个 Map<Character, Integer> 来记录每个字符的剩余次数。

2. 维护单调递增栈
  • 我们需要一个栈(这里用 StringBuilder 模拟栈)来构建最终的结果。这个栈的特点是 单调递增,也就是说,栈中的字符是按字典序从小到大排列的。
  • 遍历字符串时,对于每一个字符:
    • 如果它已经在栈中(通过 isUsed 数组标记),则直接跳过,因为题目要求去重。
    • 如果它不在栈中,则需要决定是否将其加入栈中。

3. 贪心策略:弹出栈顶字符
  • 在将当前字符加入栈之前,我们需要检查栈顶的字符是否比当前字符大,并且栈顶字符在后面还会出现(通过 map 判断剩余次数)。
    • 如果满足条件,说明栈顶字符可以被移除,因为后面还有机会再次加入它。这样做的目的是让字典序更小。
    • 弹出栈顶字符后,需要将其标记为未使用(isUsed 数组更新)。

4. 加入当前字符
  • 当栈顶字符不再比当前字符大,或者栈顶字符后面不会再出现时,将当前字符加入栈中,并标记为已使用。

5. 返回结果
  • 最终,栈中的字符就是去重后字典序最小的结果。

示例

以字符串 "cbacdcbc" 为例:

  1. 遍历到 'c',加入栈中,栈为 ['c']
  2. 遍历到 'b',发现 'c''b' 大且后面还有 'c',弹出 'c',加入 'b',栈为 ['b']
  3. 遍历到 'a',发现 'b''a' 大且后面还有 'b',弹出 'b',加入 'a',栈为 ['a']
  4. 遍历到 'c',加入栈中,栈为 ['a', 'c']
  5. 遍历到 'd',加入栈中,栈为 ['a', 'c', 'd']
  6. 遍历到 'c''c' 已经在栈中,跳过。
  7. 遍历到 'b',加入栈中,栈为 ['a', 'c', 'd', 'b']
  8. 遍历到 'c''c' 已经在栈中,跳过。

最终结果为 "acdb"


通过这种 贪心 + 单调栈 的方法,我们能够高效地解决问题,同时保证结果的字典序最小。

🌏代码实现

Java
class Solution {
    public String removeDuplicateLetters(String S) {
        // 统计每个字母的出现次数
        char[] s = S.toCharArray();
        Map<Character, Integer> map = new HashMap<>();
        for (char a : s) {
            map.put(a, map.getOrDefault(a, 0) + 1);
        }

        StringBuilder sb = new StringBuilder();
        boolean[] isUsed = new boolean[26];

        for (char sss : s) {
            map.put(sss, map.get(sss) - 1);  // 直接获取值,无需 getOrDefault
            if (isUsed[sss - 'a']) continue; // 如果已经在结果中,则跳过

            // 维护一个单调递增栈
            while (sb.length() > 0 && sb.charAt(sb.length() - 1) > sss && map.get(sb.charAt(sb.length() - 1)) > 0) {
                isUsed[sb.charAt(sb.length() - 1) - 'a'] = false; // 标记移除的字符为未使用
                sb.deleteCharAt(sb.length() - 1);
            }

            sb.append(sss);
            isUsed[sss - 'a'] = true; // 标记该字符已经被使用
        }
        return sb.toString();
    }
}

Python
class Solution(object):
    def removeDuplicateLetters(self, s):
        count = Counter(s)
        is_used = set()
        result = []

        for c in s:
            count[c] -= 1
            if c in is_used:
                continue

            while result and result[-1] > c and count[result[-1]] > 0:
                is_used.remove(result.pop())

            result.append(c)
            is_used.add(c)

        return ''.join(result)

C++
class Solution {
public:
    string removeDuplicateLetters(string s) {
        unordered_map<char, int> count;
        vector<bool> isUsed(26, false);
        for (char c : s) {
            count[c]++;
        }

        string result;
        for (char c : s) {
            count[c]--;

            if (isUsed[c - 'a']) continue;

            while (!result.empty() && result.back() > c && count[result.back()] > 0) {
                isUsed[result.back() - 'a'] = false;
                result.pop_back();
            }

            result.push_back(c);
            isUsed[c - 'a'] = true;
        }
        return result;
    }
};

结语

这三道题看似不同,但本质上都是在解决一个问题:如何构造字典序最小的字符串。它们的核心思路都是 贪心 + 单调栈,通过维护一个递增的结构,在每一步选择当前最优的字符,同时确保后续字符仍然满足条件。

  • 316 和 1081 的重点是去重,确保每个字符只出现一次,同时让字典序最小。
  • 402 则是通过删除数字来让数值最小,虽然不需要去重,但同样需要保证每一步的选择是最优的。

无论是去掉重复字符、删掉多余数字,还是优化子序列,这套方法都能帮我们高效地找到最优解。掌握了 贪心 + 单调栈 的思路,这类问题就能迎刃而解了!


如果您渴望探索更多精心挑选的高频LeetCode面试题,以及它们背后的巧妙解法,欢迎您访问我的博客,那里有我精心准备的一系列文章,旨在帮助技术爱好者们提升算法能力与编程技巧。

👉更多高频有趣LeetCode算法题

在我的博客中,每一篇文章都是我对算法世界的一次深入挖掘,不仅包含详尽的题目解析,还有我个人的心得体会、优化思路及实战经验分享。无论是准备面试还是追求技术成长,我相信这些内容都能为您提供宝贵的参考与启发。期待您的光临,让我们共同在技术之路上不断前行!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2294499.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【含开题报告+文档+PPT+源码】基于SpringBoot的校园论坛系统的设计与实现

开题报告 本研究论文主要探讨并实现了一个基于SpringBoot框架构建的全方位校园论坛系统。此系统旨在为校内师生提供一个信息交流与分享的互动平台&#xff0c;核心功能涵盖了校园新闻新闻的实时浏览与更新&#xff0c;用户可自主发布各类主题帖子&#xff0c;并支持深度互动&a…

关于视频字幕

文章目录 视频字幕分类内嵌字幕内封字幕外挂字幕 字幕格式纯文本字幕特效字幕图形字幕 简易修改字幕修改时间同步PotplayerSubtitleEdit 提取蓝光原盘字幕参考资料 视频字幕分类 内嵌字幕 合成到画面的硬字幕&#xff0c;不可移除。 内封字幕 常见的如 MKV 文件&#xff0c…

【AI 语音】实时语音交互优化全解析:从 RTC 技术到双讲处理

网罗开发 &#xff08;小红书、快手、视频号同名&#xff09; 大家好&#xff0c;我是 展菲&#xff0c;目前在上市企业从事人工智能项目研发管理工作&#xff0c;平时热衷于分享各种编程领域的软硬技能知识以及前沿技术&#xff0c;包括iOS、前端、Harmony OS、Java、Python等…

数据结构(栈结构之顺序栈操作实现一)

目录 一.栈结构之顺序栈操作实现 1.项目结构以及初始代码 2.初始化栈结构 3.入栈操作并显示 4.出栈操作并显示出栈元素 5.获取栈长度 6.清空栈 7.销毁栈 8.动态扩展栈空间 一.栈结构之顺序栈操作实现 1.项目结构以及初始代码 SeqStack.h #ifndef __SEQSTACK_H__ #de…

【React】受控组件和非受控组件

目录 受控组件非受控组件基于ref获取DOM元素1、在标签中使用2、在组件中使用 受控组件 表单元素的状态&#xff08;值&#xff09;由 React 组件的 state 完全控制。组件的 state 保存了表单元素的值&#xff0c;并且每次用户输入时&#xff0c;React 通过事件处理程序来更新 …

vue2:如何动态控制el-form-item之间的行间距

需求 某页面有查看和编辑两种状态: 编辑: 查看: 可以看到,查看时,行间距太大导致页面不紧凑,所以希望缩小查看是的行间距。 行间距设置 行间距通常是通过 CSS 的 margin 或 padding 属性来控制的。在 Element UI 的样式表中,.el-form-item 的下边距(margin-bottom)…

亚博microros小车-原生ubuntu支持系列:20 ROS Robot APP建图

依赖工程 新建工程laserscan_to_point_publisher src/laserscan_to_point_publisher/laserscan_to_point_publisher/目录下新建文件laserscan_to_point_publish.py #!/usr/bin/env python3import rclpy from rclpy.node import Node from geometry_msgs.msg import PoseStam…

计算机毕业设计Python+Vue.js游戏推荐系统 Steam游戏推荐系统 Django Flask 游 戏可视化 游戏数据分析 游戏大数据 爬虫

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

k8sollama部署deepseek-R1模型,内网无坑

这是目录 linux下载ollama模型文件下载到本地,打包迁移到k8s等无网络环境使用下载打包ollama镜像非k8s环境使用k8s部署访问方式非ollama运行deepseek模型linux下载ollama 下载后可存放其他服务器 curl -L https://ollama.com/download/ollama-linux-amd64.tgz -o ollama-linu…

【Elasticsearch】nested聚合

在 Elasticsearch 中&#xff0c;嵌套聚合&#xff08;nestedaggregation&#xff09;的语法形式用于对嵌套字段&#xff08;nestedfields&#xff09;进行聚合操作。嵌套字段是 Elasticsearch 中的一种特殊字段类型&#xff0c;用于存储数组中的对象&#xff0c;这些对象需要独…

spy-debugger + Charles 调试移动端/内嵌小程序H5

简介说明&#xff1a; PC端可以用F12进行console等进行调试&#xff0c;但移动端App中使用webview就无法进行实时调试&#xff0c;针对这种情况 1. 安装 全局安装 spy-debugger sudo npm install spy-debugger -g // window不用加sudo2. spy-debugger 证书 其实spy-debugg…

【NLP 20、Encoding编码 和 Embedding嵌入】

目录 一、核心定义与区别 二、常见Encoding编码 (1) 独热编码&#xff08;One-Hot Encoding&#xff09; (2) 位置编码&#xff08;Positional Encoding&#xff09; (3) 标签编码&#xff08;Label Encoding&#xff09; (4) 注意事项 三、常见Embedding词嵌入 (1) 基础词嵌入…

深度学习模型可视化小工具wandb

1 概述 Wandb&#xff08;Weights & Biases&#xff0c;网址是https://wandb.ai&#xff09;是一个用于机器学习项目实验跟踪、可视化和管理的工具&#xff0c;旨在用户更有效地监控模型训练过程、优化性能&#xff0c;并分享和复现实验结果‌‌。对于使用者而言&#xff…

数据库系统概论的第六版与第五版的区别,附pdf

我用夸克网盘分享了「数据库系统概论第五六版资源」&#xff0c;点击链接即可保存。 链接&#xff1a;https://pan.quark.cn/s/21a278378dee 第6版教材修订的主要内容 为了保持科学性、先进性和实用性&#xff0c;在第5版教材基础上对全书内容进行了修改、更新和充实。 在科…

【Kubernetes Pod间通信-第2篇】使用BGP实现Pod到Pod的通信

Kubernetes中Pod间的通信 本系列文章共3篇: 【Kubernetes Pod间通信-第1篇】在单个子网中使用underlay网络实现Pod到Pod的通信【Kubernetes Pod间通信-第2篇】使用BGP实现Pod到Pod的通信(本文介绍)【Kubernetes Pod间通信-第3篇】Kubernetes中Pod与ClusterIP服务之间的通信…

软件设计模式

目录 一.创建型模式 抽象工厂 Abstract Factory 构建器 Builder 工厂方法 Factory Method 原型 Prototype 单例模式 Singleton 二.结构型模式 适配器模式 Adapter 桥接模式 Bridge 组合模式 Composite 装饰者模式 Decorator 外观模式 Facade 享元模式 Flyw…

vscode 如何通过Continue引入AI 助手deepseek

第一步&#xff1a; 在deepseek 官网上注册账号&#xff0c;得到APIKeys(deepseek官网地址) 创建属于自己的APIKey,然后复制这个key,(注意保存自己的key)! 第二步&#xff1a; 打开vscode,在插件市场安装Continue插件, 点击设置&#xff0c;添加deepseek模型&#xff0c;默认…

通过docker安装部署deepseek以及python实现

前提条件 Docker 安装:确保你的系统已经安装并正确配置了 Docker。可以通过运行 docker --version 来验证 Docker 是否安装成功。 网络环境:保证设备有稳定的网络连接,以便拉取 Docker 镜像和模型文件。 步骤一:拉取 Ollama Docker 镜像 Ollama 可以帮助我们更方便地管理…

iOS 音频录制、播放与格式转换

iOS 音频录制、播放与格式转换:基于 AVFoundation 和 FFmpegKit 的实现 在 iOS 开发中,音频处理是一个非常常见的需求,比如录音、播放音频、音频格式转换等。本文将详细解读一段基于 AVFoundation 和 FFmpegKit 的代码,展示如何实现音频录制、播放以及 PCM 和 AAC 格式之间…

RK3576——USB3.2 OTG无法识别到USB设备

问题&#xff1a;使用硬盘接入到OTG接口无热插拔信息&#xff0c;接入DP显示屏无法正常识别到显示设备&#xff0c;但是能通过RKDdevTool工具烧录系统。 问题分析&#xff1a;由于热插拔功能实现是靠HUSB311芯片完成的&#xff0c;因此需要先确保HUSB311芯片驱动正常工作。 1. …