diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..7ecfd88a34 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.md linguist-detectable=true +*.md linguist-documentation=false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/.wait.md b/.github/ISSUE_TEMPLATE/.wait.md deleted file mode 100644 index 23436ee86c..0000000000 --- a/.github/ISSUE_TEMPLATE/.wait.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: 等待翻译 -about: 我想翻译的文章暂时被别人占着,我有意作为下一个占有者 -title: 'wait ' -labels: waiting -assignees: '' - ---- - - - - - - - - - -我已阅读过[翻译组工作流程](https://github.com/labuladong/fucking-algorithm/issues/9),我是按照规定的流程图选择的任务,我开启的 issue 是【等待翻译】。 - -我已阅读过[翻译要求](https://github.com/labuladong/fucking-algorithm/blob/english/README.md),我的翻译会按照要求,认真负责。 - -我正在等待如下文章的翻译工作结束(点击可查看目标文章): - - -[动态规划系列/抢房子.md](https://github.com/labuladong/fucking-algorithm/blob/master/动态规划系列/抢房子.md) - - -该文章的提交权限暂时被 #XX 占有,我已经开始了我的翻译工作,且开启了对该 issue 的 Subscribe,待上述 issue 结束后,我可能会很快占有该文章的翻译权,结合我的翻译成果和他人的成果将这篇文章翻译得更好。 - - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/01-algo-website-bug.md b/.github/ISSUE_TEMPLATE/01-algo-website-bug.md new file mode 100644 index 0000000000..78a19e2e7a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-algo-website-bug.md @@ -0,0 +1,21 @@ +--- +name: Website bug +about: Report bug on website `labuladong.online` +title: '' +labels: algo-websie-bug +assignees: labuladong + +--- + +**Network condition:** +China network or Global network? + +**Describe the bug** +A clear and concise description of what the bug is. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform** +Mobile phone or PC? +What kind of web browser? (chrome/edge/...) diff --git a/.github/ISSUE_TEMPLATE/02-algo-visualize-bug.md b/.github/ISSUE_TEMPLATE/02-algo-visualize-bug.md new file mode 100644 index 0000000000..12244361fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-algo-visualize-bug.md @@ -0,0 +1,10 @@ +--- +name: Algo-visualize bug +about: Report bug for algo-visualize tool in website/plugins +title: '' +labels: algo-visualize-bug +assignees: labuladong + +--- + + diff --git a/.github/ISSUE_TEMPLATE/03-chrome-extension-bug.md b/.github/ISSUE_TEMPLATE/03-chrome-extension-bug.md new file mode 100644 index 0000000000..f86bb86a35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-chrome-extension-bug.md @@ -0,0 +1,17 @@ +--- +name: Chrome extension bug +about: Report bug on Chrome extension +title: '' +labels: algo-website, chrome-extension-bug +assignees: labuladong + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform** +What kind of web browser are you using? (chrome/edge/...) diff --git a/.github/ISSUE_TEMPLATE/04-vscode-extension-bug.md b/.github/ISSUE_TEMPLATE/04-vscode-extension-bug.md new file mode 100644 index 0000000000..14ade83a08 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04-vscode-extension-bug.md @@ -0,0 +1,17 @@ +--- +name: VSCode extension bug +about: Report bug on vscode extension +title: '' +labels: vscode-extension-bug +assignees: labuladong + +--- + +**Version:** +What's the extension version are you using? + +**Describe the bug** +A clear and concise description of what the bug is. + +**Screenshots** +If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/05-jetbrain-plugin-bug.md b/.github/ISSUE_TEMPLATE/05-jetbrain-plugin-bug.md new file mode 100644 index 0000000000..edc29bf017 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/05-jetbrain-plugin-bug.md @@ -0,0 +1,17 @@ +--- +name: JetBrain plugin bug +about: Report bug on JetBrain plugin +title: '' +labels: jb-plugin-bug +assignees: labuladong + +--- + +**Version:** +What's the plugin version are you using? + +**Describe the bug** +A clear and concise description of what the bug is. + +**Screenshots** +If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/06-suggestion.md b/.github/ISSUE_TEMPLATE/06-suggestion.md new file mode 100644 index 0000000000..628282c5d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/06-suggestion.md @@ -0,0 +1,12 @@ +--- +name: Suggestion +about: Suggest an idea/improvement for this project +title: '' +labels: feature-request +assignees: labuladong + +--- + +Do you have any suggestions? + +Is there anything that you feel inconvenient to use? diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index df5b188691..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: 发现问题 -about: 我发现了某处链接或者知识点的错误 -title: 'bug ' -labels: bug -assignees: '' - ---- - - - -你好,我发现如下文章有 bug(点击文字可跳转到相关文章): - -[动态规划系列/抢房子.md](https://github.com/labuladong/fucking-algorithm/blob/master/动态规划系列/抢房子.md) - -问题描述: - -某章图片链接失效/其中的 XXX 内容有误/等等。 diff --git a/.github/ISSUE_TEMPLATE/others.md b/.github/ISSUE_TEMPLATE/others.md deleted file mode 100644 index f03e25ec2c..0000000000 --- a/.github/ISSUE_TEMPLATE/others.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: 其他issue -about: 我还有一些其他的建议/问题 -title: '' -labels: '' -assignees: '' - ---- - - diff --git a/.github/ISSUE_TEMPLATE/translate.md b/.github/ISSUE_TEMPLATE/translate.md deleted file mode 100644 index 2f38032020..0000000000 --- a/.github/ISSUE_TEMPLATE/translate.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: 参与翻译 -about: 我想参与仓库中文章的翻译工作 -title: 'translate ' -labels: translate -assignees: '' - ---- - - - - - - - - - -我已阅读过[翻译组工作流程](https://github.com/labuladong/fucking-algorithm/issues/9),我已阅读过[翻译要求](https://github.com/labuladong/fucking-algorithm/blob/english/README.md),我拥有了向主仓库提交 pull request 的权限,会对负责此次翻译的质量。 - -我将开始翻译如下文章(点击可查看目标文章): - - -[动态规划系列/抢房子.md](https://github.com/labuladong/fucking-algorithm/blob/master/动态规划系列/抢房子.md) - -我对如何翻译此文章已经心中有数,我准备将它翻译成:**英文** - - -**预计 3 天内翻译完成**,我会尽可能快地完成翻译,主仓库允许第一个完成的 pull request 添加翻译者昵称/姓名及 Github profile 链接(或任意你希望的链接)。 \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..63123fbe81 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_store diff --git a/README.md b/README.md index 9094d6fd69..cbd530297a 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,433 @@ -English translation is processing…… Star this repo and go back sonn:) +[![Star History Chart](https://api.star-history.com/svg?repos=labuladong/fucking-algorithm&type=Date)](https://star-history.com/#labuladong/fucking-algorithm&Date) -没想到两天就火了,招募翻译组啦,有兴趣可查看这个置顶 [issue](https://github.com/labuladong/fucking-algorithm/issues/9),成为本仓库的贡献者就是这么容易! -# 前言 +English version is on [labuladong.online](https://labuladong.online/algo/en/) too. Just enjoy:) -本仓库总共 60 多篇原创文章,基本上都是基于 LeetCode 的题目,涵盖了所有题型和技巧,而且一定要做到**举一反三,通俗易懂**,绝不是简单的代码堆砌,后面有目录。 +# labuladong 的算法笔记 + +本仓库总共 60 多篇原创文章,都是基于 LeetCode 的题目,涵盖了所有题型和技巧,而且一定要做到**举一反三,通俗易懂**,绝不是简单的代码堆砌,后面有目录。 我先吐槽几句。**刷题刷题,刷的是题,培养的是思维,本仓库的目的就是传递这种算法思维**。我要是只写一个包含 LeetCode 题目代码的仓库,有个锤子用?没有思路解释,没有思维框架,顶多写个时间复杂度,那玩意一眼就能看出来。 只想要答案的话很容易,题目评论区五花八门的答案,动不动就秀 python 一行代码解决,有那么多人点赞。问题是,你去做算法题,是去学习编程语言的奇技淫巧的,还是学习算法思维的呢?你的快乐,到底源自复制别人的一行代码通过测试,已完成题目 +1,还是源自自己通过逻辑推理和算法框架不看答案写出解法? -网上总有大佬喷我,说我写这玩意太基础了,根本没必要啰嗦。我只能说大家刷算法就是找工作吃饭的,不是打竞赛的,我也是一路摸爬滚打过来的,我们要的是清楚明白有所得,不是故弄玄虚无所指。不想办法做到通俗易懂,难道要上来先把《算法导论》吹上天,然后把人家都心怀敬仰地劝退?别的不说,公众号几万读者,PDF 版本上万次下载,联系我的出版社都好几家,说明质量还过得去吧? +网上总有大佬喷我,说我写的东西太基础,要么说不能借助框架思维来学习算法。我只能说大家刷算法就是找工作吃饭的,不是打竞赛的,我也是一路摸爬滚打过来的,我们要的是清楚明白有所得,不是故弄玄虚无所指。 + +不想办法做到通俗易懂,难道要上来先把《算法导论》吹上天,然后把人家都心怀敬仰地劝退? **做啥事情做多了,都能发现套路的,我把各种算法套路框架总结出来,相信可以帮助其他人少走弯路**。我这个纯靠自学的小童鞋,花了一年时间刷题和总结,自己写了一份算法小抄,后面有目录,这里就不废话了。 -### 使用方法 - -1、**先给本仓库点个 star,满足一下我的虚荣心**,文章质量绝对值你一个 star。我还在继续创作,给我一点继续写文的动力,感谢。 - -2、可以在我的 gitbook 上直接查看所有文章,会和公众号同步持续更新文章,建议收藏。地址:https://labuladong.gitbook.io/algo - -3、可以关注我的公众号 **labuladong** 及时获取更新。我不喜欢转载乱七八糟的低质文章,**坚持高质量原创,说是最良心最硬核的技术公众号都不为过**。 - -这些文章就是从公众号里整理出来的,目前主要发文平台是微信公众号,公众号后台回复关键词【电子书】可以获得这份小抄的电子书版本,方便你做笔记: - -brutal - -其他的先不多说了,直接上干货吧,我们一起日穿 LeetCode,感受一下支配算法的乐趣。 - -# 目录 - -* 第零章、必读系列 - * [学习算法和刷题的框架思维](算法思维系列/学习数据结构和算法的高效方法.md) - * [学习数据结构和算法读什么书](算法思维系列/为什么推荐算法4.md) - * [动态规划解题框架](动态规划系列/动态规划详解进阶.md) - * [动态规划答疑篇](动态规划系列/最优子结构.md) - * [回溯算法解题框架](算法思维系列/回溯算法详解修订版.md) - * [二分查找解题框架](算法思维系列/二分查找详解.md) - * [滑动窗口解题框架](算法思维系列/滑动窗口技巧.md) - * [双指针技巧解题框架](算法思维系列/双指针技巧.md) - * [Linux的进程、线程、文件描述符是什么](技术/linux进程.md) - * [Git/SQL/正则表达式的在线练习平台](技术/在线练习平台.md) -* 第一章、动态规划系列 - * [动态规划详解](动态规划系列/动态规划详解进阶.md) - * [动态规划答疑篇](动态规划系列/最优子结构.md) - * [动态规划设计:最长递增子序列](动态规划系列/动态规划设计:最长递增子序列.md) - * [编辑距离](动态规划系列/编辑距离.md) - * [经典动态规划问题:高楼扔鸡蛋](动态规划系列/高楼扔鸡蛋问题.md) - * [经典动态规划问题:高楼扔鸡蛋(进阶)](动态规划系列/高楼扔鸡蛋进阶.md) - * [动态规划之子序列问题解题模板](动态规划系列/子序列问题模板.md) - * [动态规划之博弈问题](动态规划系列/动态规划之博弈问题.md) - * [贪心算法之区间调度问题](动态规划系列/贪心算法之区间调度问题.md) - * [动态规划之KMP字符匹配算法](动态规划系列/动态规划之KMP字符匹配算法.md) - * [团灭 LeetCode 股票买卖问题](动态规划系列/团灭股票问题.md) - * [团灭 LeetCode 打家劫舍问题](动态规划系列/抢房子.md) - * [动态规划之四键键盘](动态规划系列/动态规划之四键键盘.md) - * [动态规划之正则表达](动态规划系列/动态规划之正则表达.md) - * [最长公共子序列](动态规划系列/最长公共子序列.md) -* 第二章、数据结构系列 - * [学习算法和刷题的思路指南](算法思维系列/学习数据结构和算法的高效方法.md) - * [学习数据结构和算法读什么书](算法思维系列/为什么推荐算法4.md) - * [二叉堆详解实现优先级队列](数据结构系列/二叉堆详解实现优先级队列.md) - * [LRU算法详解](高频面试系列/LRU算法.md) - * [二叉搜索树操作集锦](数据结构系列/二叉搜索树操作集锦.md) - * [特殊数据结构:单调栈](数据结构系列/单调栈.md) - * [特殊数据结构:单调队列](数据结构系列/单调队列.md) - * [设计Twitter](数据结构系列/设计Twitter.md) - * [递归反转链表的一部分](数据结构系列/递归反转链表的一部分.md) - * [队列实现栈\|栈实现队列](数据结构系列/队列实现栈栈实现队列.md) -* 第三章、算法思维系列 - * [算法学习之路](算法思维系列/算法学习之路.md) - * [回溯算法详解](算法思维系列/回溯算法详解修订版.md) - * [回溯算法团灭排列、组合、子集问题](高频面试系列/子集排列组合.md) - * [二分查找详解](算法思维系列/二分查找详解.md) - * [双指针技巧总结](算法思维系列/双指针技巧.md) - * [滑动窗口技巧](算法思维系列/滑动窗口技巧.md) - * [twoSum问题的核心思想](算法思维系列/twoSum问题的核心思想.md) - * [常用的位操作](算法思维系列/常用的位操作.md) - * [拆解复杂问题:实现计算器](数据结构系列/实现计算器.md) - * [烧饼排序](算法思维系列/烧饼排序.md) - * [前缀和技巧](算法思维系列/前缀和技巧.md) - * [字符串乘法](算法思维系列/字符串乘法.md) - * [FloodFill算法详解及应用](算法思维系列/FloodFill算法详解及应用.md) - * [区间调度之区间合并问题](算法思维系列/区间调度问题之区间合并.md) - * [区间调度之区间交集问题](算法思维系列/区间交集问题.md) - * [信封嵌套问题](算法思维系列/信封嵌套问题.md) - * [几个反直觉的概率问题](算法思维系列/几个反直觉的概率问题.md) - * [洗牌算法](算法思维系列/洗牌算法.md) - * [递归详解](算法思维系列/递归详解.md) -* 第四章、高频面试系列 - * [如何实现LRU算法](高频面试系列/LRU算法.md) - * [如何高效寻找素数](高频面试系列/打印素数.md) - * [如何计算编辑距离](动态规划系列/编辑距离.md) - * [如何运用二分查找算法](高频面试系列/koko偷香蕉.md) - * [如何高效解决接雨水问题](高频面试系列/接雨水.md) - * [如何去除有序数组的重复元素](高频面试系列/如何去除有序数组的重复元素.md) - * [如何寻找最长回文子串](高频面试系列/最长回文子串.md) - * [如何k个一组反转链表](高频面试系列/k个一组反转链表.md) - * [如何判定括号合法性](高频面试系列/合法括号判定.md) - * [如何寻找消失的元素](高频面试系列/消失的元素.md) - * [如何寻找缺失和重复的元素](高频面试系列/缺失和重复的元素.md) - * [如何判断回文链表](高频面试系列/判断回文链表.md) - * [如何在无限序列中随机抽取元素](高频面试系列/水塘抽样.md) - * [如何调度考生的座位](高频面试系列/座位调度.md) - * [Union-Find算法详解](算法思维系列/UnionFind算法详解.md) - * [Union-Find算法应用](算法思维系列/UnionFind算法应用.md) - * [一行代码就能解决的算法题](高频面试系列/一行代码解决的智力题.md) - * [二分查找高效判定子序列](高频面试系列/二分查找判定子序列.md) -* 第五章、计算机技术 - * [Linux的进程、线程、文件描述符是什么](技术/linux进程.md) - * [一文看懂 session 和 cookie](技术/session和cookie.md) - * [关于 Linux shell 你必须知道的](技术/linuxshell.md) - * [加密算法的前身今世](技术/密码技术.md) - * [Git/SQL/正则表达式的在线练习平台](技术/在线练习平台.md) +## 在开始学习之前 + +**1、先给本仓库点个 star,满足一下我的虚荣心**,文章质量绝对值你一个 star。我还在继续创作,给我一点继续写文的动力,感谢。 + +**2、建议收藏我的在线网站,每篇文章开头都有对应的力扣题目链接,可以边看文章边刷题,一共可以手把手带你刷 500 道题目**: + +2024 最新地址:https://labuladong.online/algo/ + +~~GitHub Pages 地址:https://labuladong.online/algo/~~ + +~~Gitee Pages 地址:https://labuladong.gitee.io/algo/~~ + +## labuladong 刷题全家桶简介 + +### 一、算法可视化面板 + +我的算法网站、所有配套插件都集成了一个算法可视化工具,可以对数据结构和递归过程进行可视化,大幅降低理解算法的难度。几乎每道题目的解法代码都有对应的可视化面板,具体参见下方介绍。 + + +### 二、学习网站 + +内容当然是我的系列算法教程中最核心的部分,我的算法教程都发布在网站 [labuladong.online](https://labuladong.online/algo/) 上,相信你会未来会在这里花费大量的学习时间,而不是仅仅加入收藏夹~ + +![](https://labuladong.github.io/pictures/简介/web_intro1.jpg) + +### 三、Chrome 插件 + +**主要功能**:Chrome 插件可以在中文版力扣或英文版 LeetCode 上快捷查看我的「题解」或「思路」,并添加了题目和算法技巧之间的引用关系,可以和我的网站/公众号/课程联动,给我的读者提供最丝滑的刷题体验。安装使用手册见下方目录。 + +![](https://labuladong.github.io/pictures/简介/chrome_intro.jpg) + + +### 四、vscode 插件 + +**主要功能**:和 Chrome 插件功能基本相同,习惯在 vscode 上刷题的读者可以使用该插件。安装使用手册见下方目录。 + +![](https://labuladong.github.io/pictures/简介/vs_intro.jpg) + + +### 五、Jetbrains 插件 + +**主要功能**:和 Chrome 插件功能基本相同,习惯在 Jetbrains 家的 IDE(PyCharm/Intellij/Goland 等)上刷题的读者可以使用该插件。安装使用手册见下方目录。 + +![](https://labuladong.github.io/pictures/简介/jb_intro.jpg) + + +最后祝大家学习愉快,在题海中自在遨游! + + +# 文章目录 + + + +### [本站简介](https://labuladong.online/algo/home/) + +### [配套插件及算法可视化](https://labuladong.online/algo/menu/tools/) + * [配套 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) + * [配套 vscode 刷题插件](https://labuladong.online/algo/intro/vscode/) + * [配套 JetBrains 刷题插件](https://labuladong.online/algo/intro/jetbrains/) + * [算法可视化面板使用说明](https://labuladong.online/algo/intro/visualize/) + * [本站付费会员](https://labuladong.online/algo/intro/site-vip/) + +### [针对初学和速成的学习规划](https://labuladong.online/algo/menu/plan/) + * [算法刷题的重点和坑](https://labuladong.online/algo/intro/how-to-learn-algorithms/) + * [初学者学习规划](https://labuladong.online/algo/intro/beginner-learning-plan/) + * [速成学习规划](https://labuladong.online/algo/intro/quick-learning-plan/) + * [习题章节的练习/复习方法](https://labuladong.online/algo/intro/how-to-practice/) + * [算法可视化速查页](https://labuladong.online/algo/intro/visualize-catalog/) + +### [入门:编程语言基础及练习](https://labuladong.online/algo/menu/) + * [本章导读](https://labuladong.online/algo/intro/programming-language-basic/) + * [C++ 语言基础](https://labuladong.online/algo/programming-language-basic/cpp/) + * [Java 语言基础](https://labuladong.online/algo/programming-language-basic/java/) + * [Golang 语言基础](https://labuladong.online/algo/programming-language-basic/golang/) + * [Python 语言基础](https://labuladong.online/algo/programming-language-basic/python/) + * [JavaScript 语言基础](https://labuladong.online/algo/intro/js/) + * [力扣/LeetCode 解题须知](https://labuladong.online/algo/intro/leetcode/) + * [编程语言刷题实践](https://labuladong.online/algo/programming-language-basic/lc-practice/) + +### [基础:数据结构及排序精讲](https://labuladong.online/algo/menu/quick-start/) + * [本章导读](https://labuladong.online/algo/intro/data-structure-basic/) + * [时间空间复杂度入门](https://labuladong.online/algo/intro/complexity-basic/) + + * [手把手带你实现动态数组](https://labuladong.online/algo/menu/dynamic-array/) + * [数组(顺序存储)基本原理](https://labuladong.online/algo/data-structure-basic/array-basic/) + * [动态数组代码实现](https://labuladong.online/algo/data-structure-basic/array-implement/) + + * [手把手带你实现单/双链表](https://labuladong.online/algo/menu/linked-list/) + * [链表(链式存储)基本原理](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) + * [链表代码实现](https://labuladong.online/algo/data-structure-basic/linkedlist-implement/) + + * [手把手带你实现队列/栈](https://labuladong.online/algo/menu/queue-stack/) + * [队列/栈基本原理](https://labuladong.online/algo/data-structure-basic/queue-stack-basic/) + * [用链表实现队列/栈](https://labuladong.online/algo/data-structure-basic/linked-queue-stack/) + * [环形数组技巧](https://labuladong.online/algo/data-structure-basic/cycle-array/) + * [用数组实现队列/栈](https://labuladong.online/algo/data-structure-basic/array-queue-stack/) + * [双端队列(Deque)原理及实现](https://labuladong.online/algo/data-structure-basic/deque-implement/) + + * [哈希表的原理及实现](https://labuladong.online/algo/menu/) + * [哈希表核心原理](https://labuladong.online/algo/data-structure-basic/hashmap-basic/) + * [用拉链法实现哈希表](https://labuladong.online/algo/data-structure-basic/hashtable-chaining/) + * [线性探查法的两个难点](https://labuladong.online/algo/data-structure-basic/linear-probing-key-point/) + * [线性探查法的两种代码实现](https://labuladong.online/algo/data-structure-basic/linear-probing-code/) + * [哈希集合的原理及代码实现](https://labuladong.online/algo/data-structure-basic/hash-set/) + + * [哈希表结构的种种变换](https://labuladong.online/algo/menu/) + * [用链表加强哈希表(LinkedHashMap)](https://labuladong.online/algo/data-structure-basic/hashtable-with-linked-list/) + * [用数组加强哈希表(ArrayHashMap)](https://labuladong.online/algo/data-structure-basic/hashtable-with-array/) + + * [二叉树结构及遍历](https://labuladong.online/algo/menu/binary-tree/) + * [二叉树基础及常见类型](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) + * [二叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) + * [多叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/n-ary-tree-traverse-basic/) + + * [二叉树结构的种种变换](https://labuladong.online/algo/menu/binary-tree/) + * [二叉搜索树的应用及可视化](https://labuladong.online/algo/data-structure-basic/tree-map-basic/) + * [红黑树的完美平衡及可视化](https://labuladong.online/algo/data-structure-basic/rbtree-basic/) + * [Trie/字典树/前缀树原理及可视化](https://labuladong.online/algo/data-structure-basic/trie-map-basic/) + * [二叉堆核心原理及可视化](https://labuladong.online/algo/data-structure-basic/binary-heap-basic/) + * [二叉堆/优先级队列代码实现](https://labuladong.online/algo/data-structure-basic/binary-heap-implement/) + * [线段树核心原理及可视化](https://labuladong.online/algo/data-structure-basic/segment-tree-basic/) + * [正在更新 ing](https://labuladong.online/algo/intro/updating/) + + * [图论数据结构及遍历](https://labuladong.online/algo/menu/graph-theory/) + * [图结构基础及通用代码实现](https://labuladong.online/algo/data-structure-basic/graph-basic/) + * [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) + * [Union Find 并查集原理](https://labuladong.online/algo/data-structure-basic/union-find-basic/) + * [正在更新 ing](https://labuladong.online/algo/intro/updating/) + + * [十大排序算法原理及可视化](https://labuladong.online/algo/menu/sorting/) + * [本章导读](https://labuladong.online/algo/intro/sorting/) + * [排序算法的关键指标](https://labuladong.online/algo/data-structure-basic/sort-basic/) + * [选择排序所面临的问题](https://labuladong.online/algo/data-structure-basic/select-sort/) + * [拥有稳定性:冒泡排序](https://labuladong.online/algo/data-structure-basic/bubble-sort/) + * [运用逆向思维:插入排序](https://labuladong.online/algo/data-structure-basic/insertion-sort/) + * [突破 O(N^2):希尔排序](https://labuladong.online/algo/data-structure-basic/shell-sort/) + * [妙用二叉树前序位置:快速排序](https://labuladong.online/algo/data-structure-basic/quick-sort/) + * [妙用二叉树后序位置:归并排序](https://labuladong.online/algo/data-structure-basic/merge-sort/) + * [二叉堆结构的运用:堆排序](https://labuladong.online/algo/data-structure-basic/heap-sort/) + * [全新的排序原理:计数排序](https://labuladong.online/algo/data-structure-basic/counting-sort/) + * [博采众长:桶排序](https://labuladong.online/algo/data-structure-basic/bucket-sort/) + * [基数排序(Radix Sort)](https://labuladong.online/algo/data-structure-basic/radix-sort/) + + * [正在更新 ing](https://labuladong.online/algo/intro/updating/) + + +### [第零章、核心刷题框架汇总](https://labuladong.online/algo/menu/core/) + * [本章导读](https://labuladong.online/algo/intro/core-intro/) + * [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + * [双指针技巧秒杀七道链表题目](https://labuladong.online/algo/essential-technique/linked-list-skills-summary/) + * [双指针技巧秒杀七道数组题目](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) + * [滑动窗口算法核心代码模板](https://labuladong.online/algo/essential-technique/sliding-window-framework/) + * [二分搜索算法核心代码模板](https://labuladong.online/algo/essential-technique/binary-search-framework/) + * [动态规划解题套路框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + * [回溯算法解题套路框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) + * [BFS 算法解题套路框架](https://labuladong.online/algo/essential-technique/bfs-framework/) + * [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + * [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) + * [贪心算法解题套路框架](https://labuladong.online/algo/essential-technique/greedy/) + * [分治算法解题套路框架](https://labuladong.online/algo/essential-technique/divide-and-conquer/) + * [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + + +### [第一章、经典数据结构算法](https://labuladong.online/algo/menu/ds/) + * [手把手刷链表算法](https://labuladong.online/algo/menu/linked-list/) + * [双指针技巧秒杀七道链表题目](https://labuladong.online/algo/essential-technique/linked-list-skills-summary/) + * [【强化练习】链表双指针经典习题](https://labuladong.online/algo/problem-set/linkedlist-two-pointers/) + * [单链表的花式反转方法汇总](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/) + * [如何判断回文链表](https://labuladong.online/algo/data-structure/palindrome-linked-list/) + + * [手把手刷数组算法](https://labuladong.online/algo/menu/array/) + * [双指针技巧秒杀七道数组题目](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) + * [二维数组的花式遍历技巧](https://labuladong.online/algo/practice-in-action/2d-array-traversal-summary/) + * [一个方法团灭 nSum 问题](https://labuladong.online/algo/practice-in-action/nsum/) + * [【强化练习】数组双指针经典习题](https://labuladong.online/algo/problem-set/array-two-pointers/) + * [小而美的算法技巧:前缀和数组](https://labuladong.online/algo/data-structure/prefix-sum/) + * [【强化练习】前缀和技巧经典习题](https://labuladong.online/algo/problem-set/perfix-sum/) + * [小而美的算法技巧:差分数组](https://labuladong.online/algo/data-structure/diff-array/) + * [滑动窗口算法核心代码模板](https://labuladong.online/algo/essential-technique/sliding-window-framework/) + * [【强化练习】滑动窗口算法经典习题](https://labuladong.online/algo/problem-set/sliding-window/) + * [滑动窗口延伸:Rabin Karp 字符匹配算法](https://labuladong.online/algo/practice-in-action/rabinkarp/) + * [二分搜索算法核心代码模板](https://labuladong.online/algo/essential-technique/binary-search-framework/) + * [实际运用二分搜索时的思维框架](https://labuladong.online/algo/frequency-interview/binary-search-in-action/) + * [【强化练习】二分搜索算法经典习题](https://labuladong.online/algo/problem-set/binary-search/) + * [带权重的随机选择算法](https://labuladong.online/algo/frequency-interview/random-pick-with-weight/) + * [田忌赛马背后的算法决策](https://labuladong.online/algo/practice-in-action/advantage-shuffle/) + + + * [手把手刷二叉树算法](https://labuladong.online/algo/menu/binary-tree/) + * [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + * [二叉树心法(思路篇)](https://labuladong.online/algo/data-structure/binary-tree-part1/) + * [二叉树心法(构造篇)](https://labuladong.online/algo/data-structure/binary-tree-part2/) + * [二叉树心法(后序篇)](https://labuladong.online/algo/data-structure/binary-tree-part3/) + * [二叉树心法(序列化篇)](https://labuladong.online/algo/data-structure/serialize-and-deserialize-binary-tree/) + * [二叉搜索树心法(特性篇)](https://labuladong.online/algo/data-structure/bst-part1/) + * [二叉搜索树心法(基操篇)](https://labuladong.online/algo/data-structure/bst-part2/) + * [二叉搜索树心法(构造篇)](https://labuladong.online/algo/data-structure/bst-part3/) + * [二叉搜索树心法(后序篇)](https://labuladong.online/algo/data-structure/bst-part4/) + + * [套模板解决 100 道二叉树习题](https://labuladong.online/algo/menu/100-bt/) + * [本章导读](https://labuladong.online/algo/intro/binary-tree-practice/) + * [【强化练习】用「遍历」思维解题 I](https://labuladong.online/algo/problem-set/binary-tree-traverse-i/) + * [【强化练习】用「遍历」思维解题 II](https://labuladong.online/algo/problem-set/binary-tree-traverse-ii/) + * [【强化练习】用「遍历」思维解题 III](https://labuladong.online/algo/problem-set/binary-tree-traverse-iii/) + * [【强化练习】用「分解问题」思维解题 I](https://labuladong.online/algo/problem-set/binary-tree-divide-i/) + * [【强化练习】用「分解问题」思维解题 II](https://labuladong.online/algo/problem-set/binary-tree-divide-ii/) + * [【强化练习】同时运用两种思维解题](https://labuladong.online/algo/problem-set/binary-tree-combine-two-view/) + * [【强化练习】利用后序位置解题 I](https://labuladong.online/algo/problem-set/binary-tree-post-order-i/) + * [【强化练习】利用后序位置解题 II](https://labuladong.online/algo/problem-set/binary-tree-post-order-ii/) + * [【强化练习】利用后序位置解题 III](https://labuladong.online/algo/problem-set/binary-tree-post-order-iii/) + * [【强化练习】运用层序遍历解题 I](https://labuladong.online/algo/problem-set/binary-tree-level-i/) + * [【强化练习】运用层序遍历解题 II](https://labuladong.online/algo/problem-set/binary-tree-level-ii/) + * [【强化练习】二叉搜索树经典例题 I](https://labuladong.online/algo/problem-set/bst1/) + * [【强化练习】二叉搜索树经典例题 II](https://labuladong.online/algo/problem-set/bst2/) + + * [二叉树的拓展延伸](https://labuladong.online/algo/menu/more-bt/) + * [拓展:最近公共祖先系列解题框架](https://labuladong.online/algo/practice-in-action/lowest-common-ancestor-summary/) + * [拓展:如何计算完全二叉树的节点数](https://labuladong.online/algo/data-structure/count-complete-tree-nodes/) + * [拓展:惰性展开多叉树](https://labuladong.online/algo/data-structure/flatten-nested-list-iterator/) + * [拓展:归并排序详解及应用](https://labuladong.online/algo/practice-in-action/merge-sort/) + * [拓展:快速排序详解及应用](https://labuladong.online/algo/practice-in-action/quick-sort/) + * [拓展:用栈模拟递归迭代遍历二叉树](https://labuladong.online/algo/data-structure/iterative-traversal-binary-tree/) + + * [手把手设计数据结构](https://labuladong.online/algo/menu/design/) + * [队列实现栈以及栈实现队列](https://labuladong.online/algo/data-structure/stack-queue/) + * [【强化练习】栈的经典习题](https://labuladong.online/algo/problem-set/stack/) + * [【强化练习】括号类问题汇总](https://labuladong.online/algo/problem-set/parentheses/) + * [【强化练习】队列的经典习题](https://labuladong.online/algo/problem-set/queue/) + * [单调栈算法模板解决三道例题](https://labuladong.online/algo/data-structure/monotonic-stack/) + * [【强化练习】单调栈的几种变体及经典习题](https://labuladong.online/algo/problem-set/monotonic-stack/) + * [单调队列结构解决滑动窗口问题](https://labuladong.online/algo/data-structure/monotonic-queue/) + * [【强化练习】单调队列的通用实现及经典习题](https://labuladong.online/algo/problem-set/monotonic-queue/) + * [算法就像搭乐高:手撸 LRU 算法](https://labuladong.online/algo/data-structure/lru-cache/) + * [算法就像搭乐高:手撸 LFU 算法](https://labuladong.online/algo/frequency-interview/lfu/) + * [常数时间删除/查找数组中的任意元素](https://labuladong.online/algo/data-structure/random-set/) + * [【强化练习】哈希表更多习题](https://labuladong.online/algo/problem-set/hash-table/) + * [【强化练习】优先级队列经典习题](https://labuladong.online/algo/problem-set/binary-heap/) + * [TreeMap/TreeSet 代码实现](https://labuladong.online/algo/data-structure-basic/tree-map-implement/) + * [SegmentTree 线段树代码实现](https://labuladong.online/algo/data-structure/segment-tree-implement/) + * [Trie/字典树/前缀树代码实现](https://labuladong.online/algo/data-structure/trie-implement/) + * [【强化练习】Trie 树算法习题](https://labuladong.online/algo/problem-set/trie/) + * [设计朋友圈时间线功能](https://labuladong.online/algo/data-structure/design-twitter/) + * [设计考场座位分配算法](https://labuladong.online/algo/frequency-interview/exam-room/) + * [【强化练习】更多经典设计习题](https://labuladong.online/algo/problem-set/ds-design/) + * [拓展:如何实现一个计算器](https://labuladong.online/algo/data-structure/implement-calculator/) + * [拓展:两个二叉堆实现中位数算法](https://labuladong.online/algo/practice-in-action/find-median-from-data-stream/) + * [拓展:数组去重问题(困难版)](https://labuladong.online/algo/frequency-interview/remove-duplicate-letters/) + + + * [手把手刷图算法](https://labuladong.online/algo/menu/graph/) + * [环检测及拓扑排序算法](https://labuladong.online/algo/data-structure/topological-sort/) + * [众里寻他千百度:名流问题](https://labuladong.online/algo/frequency-interview/find-celebrity/) + * [二分图判定算法](https://labuladong.online/algo/data-structure/bipartite-graph/) + * [Union-Find 并查集算法](https://labuladong.online/algo/data-structure/union-find/) + * [【强化练习】并查集经典习题](https://labuladong.online/algo/problem-set/union-find/) + * [Kruskal 最小生成树算法](https://labuladong.online/algo/data-structure/kruskal/) + * [Prim 最小生成树算法](https://labuladong.online/algo/data-structure/prim/) + * [Dijkstra 算法模板及应用](https://labuladong.online/algo/data-structure/dijkstra/) + * [【强化练习】Dijkstra 算法经典习题](https://labuladong.online/algo/problem-set/dijkstra/) + +### [第二章、经典暴力搜索算法](https://labuladong.online/algo/menu/braute-force-search/) + * [DFS/回溯算法](https://labuladong.online/algo/menu/dfs/) + * [回溯算法解题套路框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) + * [回溯算法实践:数独和 N 皇后问题](https://labuladong.online/algo/practice-in-action/sudoku-nqueue/) + * [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) + * [球盒模型:回溯算法穷举的两种视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) + * [解答回溯算法/DFS算法的若干疑问](https://labuladong.online/algo/essential-technique/backtrack-vs-dfs/) + * [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) + * [回溯算法实践:括号生成](https://labuladong.online/algo/practice-in-action/generate-parentheses/) + * [回溯算法实践:集合划分](https://labuladong.online/algo/practice-in-action/partition-to-k-equal-sum-subsets/) + * [【强化练习】回溯算法经典习题 I](https://labuladong.online/algo/problem-set/backtrack-i/) + * [【强化练习】回溯算法经典习题 II](https://labuladong.online/algo/problem-set/backtrack-ii/) + * [【强化练习】回溯算法经典习题 III](https://labuladong.online/algo/problem-set/backtrack-iii/) + + * [BFS 算法](https://labuladong.online/algo/menu/bfs/) + * [BFS 算法解题套路框架](https://labuladong.online/algo/essential-technique/bfs-framework/) + * [【强化练习】BFS 经典习题 I](https://labuladong.online/algo/problem-set/bfs/) + * [【强化练习】BFS 经典习题 II](https://labuladong.online/algo/problem-set/bfs-ii/) + * [正在更新 ing](https://labuladong.online/algo/intro/updating/) + + +### [第三章、经典动态规划算法](https://labuladong.online/algo/menu/dp/) + * [动态规划基本技巧](https://labuladong.online/algo/menu/dp-basic/) + * [动态规划解题套路框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + * [动态规划设计:最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) + * [base case 和备忘录的初始值怎么定?](https://labuladong.online/algo/dynamic-programming/memo-fundamental/) + * [动态规划穷举的两种视角](https://labuladong.online/algo/dynamic-programming/two-views-of-dp/) + * [动态规划和回溯算法的思维转换](https://labuladong.online/algo/dynamic-programming/word-break/) + * [对动态规划进行降维打击](https://labuladong.online/algo/dynamic-programming/space-optimization/) + * [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + + * [子序列类型问题](https://labuladong.online/algo/menu/subsequence/) + * [经典动态规划:编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/) + * [动态规划设计:最大子数组](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) + * [经典动态规划:最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/) + * [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) + + * [背包类型问题](https://labuladong.online/algo/menu/knapsack/) + * [经典动态规划:0-1 背包问题](https://labuladong.online/algo/dynamic-programming/knapsack1/) + * [经典动态规划:子集背包问题](https://labuladong.online/algo/dynamic-programming/knapsack2/) + * [经典动态规划:完全背包问题](https://labuladong.online/algo/dynamic-programming/knapsack3/) + * [背包问题的变体:目标和](https://labuladong.online/algo/dynamic-programming/target-sum/) + + * [用动态规划玩游戏](https://labuladong.online/algo/menu/dp-game/) + * [动态规划之最小路径和](https://labuladong.online/algo/dynamic-programming/minimum-path-sum/) + * [动态规划帮我通关了《魔塔》](https://labuladong.online/algo/dynamic-programming/magic-tower/) + * [动态规划帮我通关了《辐射4》](https://labuladong.online/algo/dynamic-programming/freedom-trail/) + * [旅游省钱大法:加权最短路径](https://labuladong.online/algo/dynamic-programming/cheap-travel/) + * [经典动态规划:正则表达式](https://labuladong.online/algo/dynamic-programming/regular-expression-matching/) + * [经典动态规划:高楼扔鸡蛋](https://labuladong.online/algo/dynamic-programming/egg-drop/) + * [经典动态规划:戳气球](https://labuladong.online/algo/dynamic-programming/burst-balloons/) + * [经典动态规划:博弈问题](https://labuladong.online/algo/dynamic-programming/game-theory/) + * [一个方法团灭 LeetCode 打家劫舍问题](https://labuladong.online/algo/dynamic-programming/house-robber/) + * [一个方法团灭 LeetCode 股票买卖问题](https://labuladong.online/algo/dynamic-programming/stock-problem-summary/) + + * [贪心类型问题](https://labuladong.online/algo/menu/greedy/) + * [贪心算法解题套路框架](https://labuladong.online/algo/essential-technique/greedy/) + * [老司机加油算法](https://labuladong.online/algo/frequency-interview/gas-station-greedy/) + * [贪心算法之区间调度问题](https://labuladong.online/algo/frequency-interview/interval-scheduling/) + * [扫描线技巧:安排会议室](https://labuladong.online/algo/frequency-interview/scan-line-technique/) + * [剪视频剪出一个贪心算法](https://labuladong.online/algo/frequency-interview/cut-video/) + + +### [第四章、其他常见算法技巧](https://labuladong.online/algo/menu/other/) + * [数学运算技巧](https://labuladong.online/algo/menu/math/) + * [一行代码就能解决的算法题](https://labuladong.online/algo/frequency-interview/one-line-solutions/) + * [常用的位操作](https://labuladong.online/algo/frequency-interview/bitwise-operation/) + * [谈谈游戏中的随机算法](https://labuladong.online/algo/frequency-interview/random-algorithm/) + * [讲两道常考的阶乘算法题](https://labuladong.online/algo/frequency-interview/factorial-problems/) + * [如何高效寻找素数](https://labuladong.online/algo/frequency-interview/print-prime-number/) + * [如何高效进行模幂运算](https://labuladong.online/algo/frequency-interview/quick-power/) + * [如何同时寻找缺失和重复的元素](https://labuladong.online/algo/frequency-interview/mismatch-set/) + * [几个反直觉的概率问题](https://labuladong.online/algo/frequency-interview/probability-problem/) + * [【强化练习】数学技巧相关习题](https://labuladong.online/algo/problem-set/math-tricks/) + + * [经典面试题](https://labuladong.online/algo/menu/interview/) + * [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + * [如何高效解决接雨水问题](https://labuladong.online/algo/frequency-interview/trapping-rain-water/) + * [一文秒杀所有丑数系列问题](https://labuladong.online/algo/frequency-interview/ugly-number-summary/) + * [一个方法解决三道区间问题](https://labuladong.online/algo/practice-in-action/interval-problem-summary/) + * [谁能想到,斗地主也能玩出算法](https://labuladong.online/algo/practice-in-action/split-array-into-consecutive-subsequences/) + * [烧饼排序算法](https://labuladong.online/algo/frequency-interview/pancake-sorting/) + * [字符串乘法计算](https://labuladong.online/algo/practice-in-action/multiply-strings/) + * [如何判定完美矩形](https://labuladong.online/algo/frequency-interview/perfect-rectangle/) + +### [附录](https://labuladong.online/algo/menu/appendix/) + * [labuladong.online 更新日志](https://labuladong.online/algo/changelog/website/) + * [可视化面板更新日志](https://labuladong.online/algo/changelog/visualize/) + * [Chrome 刷题插件更新日志](https://labuladong.online/algo/changelog/chrome/) + * [vscode 刷题插件更新日志](https://labuladong.online/algo/changelog/vscode/) + * [Jetbrain 刷题插件更新日志](https://labuladong.online/algo/changelog/jetbrain/) + * [网站/插件问题反馈](https://labuladong.online/algo/intro/bug-report/) + + + +# 感谢如下大佬参与翻译 + +按照昵称字典序排名: + +[ABCpril](https://github.com/ABCpril), +[andavid](https://github.com/andavid), +[bryceustc](https://github.com/bryceustc), +[build2645](https://github.com/build2645), +[CarrieOn](https://github.com/CarrieOn), +[cooker](https://github.com/xiaochuhub), +[Dong Wang](https://github.com/Coder2Programmer), +[ExcaliburEX](https://github.com/ExcaliburEX), +[floatLig](https://github.com/floatLig), +[ForeverSolar](https://github.com/foreversolar), +[Fulin Li](https://fulinli.github.io/), +[Funnyyanne](https://github.com/Funnyyanne), +[GYHHAHA](https://github.com/GYHHAHA), +[Hi_archer](https://hiarcher.top/), +[Iruze](https://github.com/Iruze), +[Jieyixia](https://github.com/Jieyixia), +[Justin](https://github.com/Justin-YGG), +[Kevin](https://github.com/Kevin-free), +[Lrc123](https://github.com/Lrc123), +[lriy](https://github.com/lriy), +[Lyjeeq](https://github.com/Lyjeeq), +[MasonShu](https://greenwichmt.github.io/), +[Master-cai](https://github.com/Master-cai), +[miaoxiaozui2017](https://github.com/miaoxiaozui2017), +[natsunoyoru97](https://github.com/natsunoyoru97), +[nettee](https://github.com/nettee), +[PaperJets](https://github.com/PaperJets), +[qy-yang](https://github.com/qy-yang), +[realism0331](https://github.com/realism0331), +[SCUhzs](https://github.com/brucecat), +[Seaworth](https://github.com/Seaworth), +[shazi4399](https://github.com/shazi4399), +[ShuozheLi](https://github.com/ShuoZheLi/), +[sinjoywong](https://blog.csdn.net/SinjoyWong), +[sunqiuming526](https://github.com/sunqiuming526), +[Tianhao Zhou](https://github.com/tianhaoz95), +[timmmGZ](https://github.com/timmmGZ), +[tommytim0515](https://github.com/tommytim0515), +[ucsk](https://github.com/ucsk), +[wadegrc](https://github.com/wadegrc), +[walsvid](https://github.com/walsvid), +[warmingkkk](https://github.com/warmingkkk), +[Wonderxie](https://github.com/Wonderxie), +[wsyzxxxx](https://github.com/wsyzxxxx), +[xiaodp](https://github.com/xiaodp), +[youyun](https://github.com/youyun), +[yx-tan](https://github.com/yx-tan), +[Zero](https://github.com/Mr2er0), +[Ziming](https://github.com/ML-ZimingMeng/LeetCode-Python3) # Donate -brutal +如果本仓库对你有帮助,可以请作者喝杯速溶咖啡 + + diff --git a/contributor.jpg b/contributor.jpg new file mode 100644 index 0000000000..716a4a79a0 Binary files /dev/null and b/contributor.jpg differ diff --git a/pictures/4keyboard/1.jpg b/pictures/4keyboard/1.jpg index e7e543e234..43c7985b4e 100644 Binary files a/pictures/4keyboard/1.jpg and b/pictures/4keyboard/1.jpg differ diff --git a/pictures/4keyboard/title.png b/pictures/4keyboard/title.png index b5b71adb9e..6413dee2c8 100644 Binary files a/pictures/4keyboard/title.png and b/pictures/4keyboard/title.png differ diff --git a/pictures/BST/BST_example.png b/pictures/BST/BST_example.png index 5306f18673..3e150d1d99 100644 Binary files a/pictures/BST/BST_example.png and b/pictures/BST/BST_example.png differ diff --git a/pictures/BST/bst_deletion_case_1.png b/pictures/BST/bst_deletion_case_1.png index 46dcfabda2..923f5e85d2 100644 Binary files a/pictures/BST/bst_deletion_case_1.png and b/pictures/BST/bst_deletion_case_1.png differ diff --git a/pictures/BST/bst_deletion_case_2.png b/pictures/BST/bst_deletion_case_2.png index 6edd38d83c..c8c8217797 100644 Binary files a/pictures/BST/bst_deletion_case_2.png and b/pictures/BST/bst_deletion_case_2.png differ diff --git a/pictures/BST/bst_deletion_case_3.png b/pictures/BST/bst_deletion_case_3.png index 78473707ca..1a13586540 100644 Binary files a/pictures/BST/bst_deletion_case_3.png and b/pictures/BST/bst_deletion_case_3.png differ diff --git "a/pictures/BST/\345\201\207BST.png" "b/pictures/BST/\345\201\207BST.png" index 3028c69c28..47ac2b32fa 100644 Binary files "a/pictures/BST/\345\201\207BST.png" and "b/pictures/BST/\345\201\207BST.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/baidumonkey.png" "b/pictures/Chrome\346\217\222\344\273\266/baidumonkey.png" index 3dfb9c5287..b02f566d23 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/baidumonkey.png" and "b/pictures/Chrome\346\217\222\344\273\266/baidumonkey.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/baidu\345\271\277\345\221\212.png" "b/pictures/Chrome\346\217\222\344\273\266/baidu\345\271\277\345\221\212.png" index ee8dfe2706..1af676e9e7 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/baidu\345\271\277\345\221\212.png" and "b/pictures/Chrome\346\217\222\344\273\266/baidu\345\271\277\345\221\212.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/csdnBlock.png" "b/pictures/Chrome\346\217\222\344\273\266/csdnBlock.png" index ae129f8b5b..8b57fedc8a 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/csdnBlock.png" and "b/pictures/Chrome\346\217\222\344\273\266/csdnBlock.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/githubzip.png" "b/pictures/Chrome\346\217\222\344\273\266/githubzip.png" index 3dfb74cd56..41fbf53a1e 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/githubzip.png" and "b/pictures/Chrome\346\217\222\344\273\266/githubzip.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/listen1.png" "b/pictures/Chrome\346\217\222\344\273\266/listen1.png" index 034f236e75..0af8e0bb34 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/listen1.png" and "b/pictures/Chrome\346\217\222\344\273\266/listen1.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/monkey.png" "b/pictures/Chrome\346\217\222\344\273\266/monkey.png" index 194d64deae..6b3545cbee 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/monkey.png" and "b/pictures/Chrome\346\217\222\344\273\266/monkey.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/oneTab.png" "b/pictures/Chrome\346\217\222\344\273\266/oneTab.png" index 7c033bdb9c..ad84073735 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/oneTab.png" and "b/pictures/Chrome\346\217\222\344\273\266/oneTab.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/pin.png" "b/pictures/Chrome\346\217\222\344\273\266/pin.png" index eeeb0061c3..045d79b045 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/pin.png" and "b/pictures/Chrome\346\217\222\344\273\266/pin.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/tree.png" "b/pictures/Chrome\346\217\222\344\273\266/tree.png" index 6df887f0c5..eeaa173338 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/tree.png" and "b/pictures/Chrome\346\217\222\344\273\266/tree.png" differ diff --git "a/pictures/Chrome\346\217\222\344\273\266/youhou\344\274\230\345\214\226.png" "b/pictures/Chrome\346\217\222\344\273\266/youhou\344\274\230\345\214\226.png" index 73de685316..ae944dd791 100644 Binary files "a/pictures/Chrome\346\217\222\344\273\266/youhou\344\274\230\345\214\226.png" and "b/pictures/Chrome\346\217\222\344\273\266/youhou\344\274\230\345\214\226.png" differ diff --git a/pictures/LCS/1.png b/pictures/LCS/1.png index db53c702e8..c3fa9fcf09 100644 Binary files a/pictures/LCS/1.png and b/pictures/LCS/1.png differ diff --git a/pictures/LCS/2.png b/pictures/LCS/2.png index f8a4ecb0cf..8d1db3538d 100644 Binary files a/pictures/LCS/2.png and b/pictures/LCS/2.png differ diff --git a/pictures/LCS/3.png b/pictures/LCS/3.png index eeb5489913..ed7a3f91fd 100644 Binary files a/pictures/LCS/3.png and b/pictures/LCS/3.png differ diff --git a/pictures/LCS/dp.png b/pictures/LCS/dp.png index 7df4b5524e..163f88307f 100644 Binary files a/pictures/LCS/dp.png and b/pictures/LCS/dp.png differ diff --git a/pictures/LCS/lcs.png b/pictures/LCS/lcs.png index b7ff2d01b6..b0a76e29a2 100644 Binary files a/pictures/LCS/lcs.png and b/pictures/LCS/lcs.png differ diff --git "a/pictures/LRU\347\256\227\346\263\225/1.jpg" "b/pictures/LRU\347\256\227\346\263\225/1.jpg" index df7c9560d0..2cd627bacf 100644 Binary files "a/pictures/LRU\347\256\227\346\263\225/1.jpg" and "b/pictures/LRU\347\256\227\346\263\225/1.jpg" differ diff --git "a/pictures/LRU\347\256\227\346\263\225/2.jpg" "b/pictures/LRU\347\256\227\346\263\225/2.jpg" index c9a407eef4..a774ec5f1f 100644 Binary files "a/pictures/LRU\347\256\227\346\263\225/2.jpg" and "b/pictures/LRU\347\256\227\346\263\225/2.jpg" differ diff --git "a/pictures/LRU\347\256\227\346\263\225/3.jpg" "b/pictures/LRU\347\256\227\346\263\225/3.jpg" index 9bdea69c14..d61f20690a 100644 Binary files "a/pictures/LRU\347\256\227\346\263\225/3.jpg" and "b/pictures/LRU\347\256\227\346\263\225/3.jpg" differ diff --git "a/pictures/LRU\347\256\227\346\263\225/4.jpg" "b/pictures/LRU\347\256\227\346\263\225/4.jpg" index ce1a22a3f4..30af4fb8bf 100644 Binary files "a/pictures/LRU\347\256\227\346\263\225/4.jpg" and "b/pictures/LRU\347\256\227\346\263\225/4.jpg" differ diff --git "a/pictures/LRU\347\256\227\346\263\225/put.jpg" "b/pictures/LRU\347\256\227\346\263\225/put.jpg" new file mode 100644 index 0000000000..ca580eb634 Binary files /dev/null and "b/pictures/LRU\347\256\227\346\263\225/put.jpg" differ diff --git a/pictures/algo4/1.jpg b/pictures/algo4/1.jpg index bc5c41c00e..fbb9b05013 100644 Binary files a/pictures/algo4/1.jpg and b/pictures/algo4/1.jpg differ diff --git a/pictures/algo4/2.jpg b/pictures/algo4/2.jpg index 533f783eb6..853c5c7147 100644 Binary files a/pictures/algo4/2.jpg and b/pictures/algo4/2.jpg differ diff --git a/pictures/algo4/3.jpg b/pictures/algo4/3.jpg index e317098a2c..0dec5070f9 100644 Binary files a/pictures/algo4/3.jpg and b/pictures/algo4/3.jpg differ diff --git a/pictures/algo4/title.png b/pictures/algo4/title.png index aabe966dfb..988f8b5737 100644 Binary files a/pictures/algo4/title.png and b/pictures/algo4/title.png differ diff --git a/pictures/backtrack/ink-image (1).png b/pictures/backtrack/ink-image (1).png index 1cf2957d12..b0df286974 100644 Binary files a/pictures/backtrack/ink-image (1).png and b/pictures/backtrack/ink-image (1).png differ diff --git a/pictures/backtrack/ink-image (2).png b/pictures/backtrack/ink-image (2).png index bf791a9117..5aa69b5c21 100644 Binary files a/pictures/backtrack/ink-image (2).png and b/pictures/backtrack/ink-image (2).png differ diff --git a/pictures/backtrack/ink-image (3).png b/pictures/backtrack/ink-image (3).png index 0ffa62c1d6..dd12af2bc3 100644 Binary files a/pictures/backtrack/ink-image (3).png and b/pictures/backtrack/ink-image (3).png differ diff --git a/pictures/backtrack/ink-image (4).png b/pictures/backtrack/ink-image (4).png index 6dcafa1beb..6c0c54d307 100644 Binary files a/pictures/backtrack/ink-image (4).png and b/pictures/backtrack/ink-image (4).png differ diff --git a/pictures/backtrack/ink-image (5).png b/pictures/backtrack/ink-image (5).png index e4d2130ee1..eb73a4dd5a 100644 Binary files a/pictures/backtrack/ink-image (5).png and b/pictures/backtrack/ink-image (5).png differ diff --git a/pictures/backtrack/ink-image (6).png b/pictures/backtrack/ink-image (6).png index f6fc968bea..75c0f4a0f6 100644 Binary files a/pictures/backtrack/ink-image (6).png and b/pictures/backtrack/ink-image (6).png differ diff --git a/pictures/backtrack/ink-image.png b/pictures/backtrack/ink-image.png index ec1ea64d50..5440b0667c 100644 Binary files a/pictures/backtrack/ink-image.png and b/pictures/backtrack/ink-image.png differ diff --git a/pictures/backtrack/nqueens.png b/pictures/backtrack/nqueens.png index 4c9e01f560..464ff529ab 100644 Binary files a/pictures/backtrack/nqueens.png and b/pictures/backtrack/nqueens.png differ diff --git a/pictures/backtrack/permutation.png b/pictures/backtrack/permutation.png index 421a36e67e..d9f4c6d325 100644 Binary files a/pictures/backtrack/permutation.png and b/pictures/backtrack/permutation.png differ diff --git "a/pictures/backtrack/\344\273\243\347\240\201.png" "b/pictures/backtrack/\344\273\243\347\240\201.png" index 85be66b34e..25d7c88cdd 100644 Binary files "a/pictures/backtrack/\344\273\243\347\240\201.png" and "b/pictures/backtrack/\344\273\243\347\240\201.png" differ diff --git "a/pictures/backtrack/\344\273\243\347\240\2011.png" "b/pictures/backtrack/\344\273\243\347\240\2011.png" index b029f5f776..f156208f94 100644 Binary files "a/pictures/backtrack/\344\273\243\347\240\2011.png" and "b/pictures/backtrack/\344\273\243\347\240\2011.png" differ diff --git "a/pictures/backtrack/\344\273\243\347\240\2012.png" "b/pictures/backtrack/\344\273\243\347\240\2012.png" index ae4a5734ee..c1671f1bfd 100644 Binary files "a/pictures/backtrack/\344\273\243\347\240\2012.png" and "b/pictures/backtrack/\344\273\243\347\240\2012.png" differ diff --git "a/pictures/backtrack/\344\273\243\347\240\2013.png" "b/pictures/backtrack/\344\273\243\347\240\2013.png" index c6d842db5e..0dc5eaf30b 100644 Binary files "a/pictures/backtrack/\344\273\243\347\240\2013.png" and "b/pictures/backtrack/\344\273\243\347\240\2013.png" differ diff --git "a/pictures/backtrack/\345\205\250\346\216\222\345\210\227.png" "b/pictures/backtrack/\345\205\250\346\216\222\345\210\227.png" index 6fb4f92f97..e7cc064f0a 100644 Binary files "a/pictures/backtrack/\345\205\250\346\216\222\345\210\227.png" and "b/pictures/backtrack/\345\205\250\346\216\222\345\210\227.png" differ diff --git a/pictures/backtracking/1.jpg b/pictures/backtracking/1.jpg index e54c8a3c20..b847370747 100644 Binary files a/pictures/backtracking/1.jpg and b/pictures/backtracking/1.jpg differ diff --git a/pictures/backtracking/2.jpg b/pictures/backtracking/2.jpg index ff57c7fdd3..2bca873678 100644 Binary files a/pictures/backtracking/2.jpg and b/pictures/backtracking/2.jpg differ diff --git a/pictures/backtracking/3.jpg b/pictures/backtracking/3.jpg index ca97cdaa03..b016864ae8 100644 Binary files a/pictures/backtracking/3.jpg and b/pictures/backtracking/3.jpg differ diff --git a/pictures/backtracking/4.jpg b/pictures/backtracking/4.jpg index b93cafd72e..3690d9637c 100644 Binary files a/pictures/backtracking/4.jpg and b/pictures/backtracking/4.jpg differ diff --git a/pictures/backtracking/5.jpg b/pictures/backtracking/5.jpg index 893c87e682..727ed1ecd0 100644 Binary files a/pictures/backtracking/5.jpg and b/pictures/backtracking/5.jpg differ diff --git a/pictures/backtracking/6.jpg b/pictures/backtracking/6.jpg index 0b4d68eea5..368dee24ba 100644 Binary files a/pictures/backtracking/6.jpg and b/pictures/backtracking/6.jpg differ diff --git a/pictures/backtracking/7.jpg b/pictures/backtracking/7.jpg index 1eee2f8c36..a146fd6b92 100644 Binary files a/pictures/backtracking/7.jpg and b/pictures/backtracking/7.jpg differ diff --git a/pictures/calculator/1.1.jpg b/pictures/calculator/1.1.jpg index 100ca61a10..2ead09ba04 100644 Binary files a/pictures/calculator/1.1.jpg and b/pictures/calculator/1.1.jpg differ diff --git a/pictures/calculator/1.jpg b/pictures/calculator/1.jpg index 862bbdd41c..c66e7ab379 100644 Binary files a/pictures/calculator/1.jpg and b/pictures/calculator/1.jpg differ diff --git a/pictures/calculator/2.jpg b/pictures/calculator/2.jpg index 62e40b92e8..f3123387da 100644 Binary files a/pictures/calculator/2.jpg and b/pictures/calculator/2.jpg differ diff --git a/pictures/calculator/3.jpg b/pictures/calculator/3.jpg index 073f6e39c4..2f3b9a4247 100644 Binary files a/pictures/calculator/3.jpg and b/pictures/calculator/3.jpg differ diff --git a/pictures/calculator/4.jpg b/pictures/calculator/4.jpg index 0b0c9ce4b6..4f878ed3b7 100644 Binary files a/pictures/calculator/4.jpg and b/pictures/calculator/4.jpg differ diff --git a/pictures/calculator/5.jpg b/pictures/calculator/5.jpg index c1610cc021..528309bb67 100644 Binary files a/pictures/calculator/5.jpg and b/pictures/calculator/5.jpg differ diff --git a/pictures/calculator/6.jpg b/pictures/calculator/6.jpg index 92800a3ece..f0563fd2e5 100644 Binary files a/pictures/calculator/6.jpg and b/pictures/calculator/6.jpg differ diff --git a/pictures/cover.jpg b/pictures/cover.jpg new file mode 100644 index 0000000000..646769e6cc Binary files /dev/null and b/pictures/cover.jpg differ diff --git a/pictures/dupmissing/2.jpg b/pictures/dupmissing/2.jpg index 781e142ade..91fa41a3c2 100644 Binary files a/pictures/dupmissing/2.jpg and b/pictures/dupmissing/2.jpg differ diff --git a/pictures/dupmissing/3.jpg b/pictures/dupmissing/3.jpg index 00d90d6794..babdd3f3c3 100644 Binary files a/pictures/dupmissing/3.jpg and b/pictures/dupmissing/3.jpg differ diff --git a/pictures/editDistance/1.jpg b/pictures/editDistance/1.jpg index 7a2e3d58f4..3f2f0b50ad 100644 Binary files a/pictures/editDistance/1.jpg and b/pictures/editDistance/1.jpg differ diff --git a/pictures/editDistance/2.jpg b/pictures/editDistance/2.jpg index 0e32faa854..1339bd16b8 100644 Binary files a/pictures/editDistance/2.jpg and b/pictures/editDistance/2.jpg differ diff --git a/pictures/editDistance/3.jpg b/pictures/editDistance/3.jpg index c201c6daa2..387c16886a 100644 Binary files a/pictures/editDistance/3.jpg and b/pictures/editDistance/3.jpg differ diff --git a/pictures/editDistance/4.jpg b/pictures/editDistance/4.jpg index 13631d1edf..a80c026ae2 100644 Binary files a/pictures/editDistance/4.jpg and b/pictures/editDistance/4.jpg differ diff --git a/pictures/editDistance/5.jpg b/pictures/editDistance/5.jpg index 8f5c006323..9106380751 100644 Binary files a/pictures/editDistance/5.jpg and b/pictures/editDistance/5.jpg differ diff --git a/pictures/editDistance/6.jpg b/pictures/editDistance/6.jpg index 589f8aef6b..5880bae6b4 100644 Binary files a/pictures/editDistance/6.jpg and b/pictures/editDistance/6.jpg differ diff --git a/pictures/editDistance/dp.jpg b/pictures/editDistance/dp.jpg index 41e72a4903..4519d891e6 100644 Binary files a/pictures/editDistance/dp.jpg and b/pictures/editDistance/dp.jpg differ diff --git a/pictures/editDistance/title.png b/pictures/editDistance/title.png index 85a392fbde..15dc923107 100644 Binary files a/pictures/editDistance/title.png and b/pictures/editDistance/title.png differ diff --git a/pictures/floodfill/floodfill.png b/pictures/floodfill/floodfill.png index 14eebee95b..3c79709bce 100644 Binary files a/pictures/floodfill/floodfill.png and b/pictures/floodfill/floodfill.png differ diff --git a/pictures/floodfill/leetcode.png b/pictures/floodfill/leetcode.png index 52f723e266..1ffdc937a7 100644 Binary files a/pictures/floodfill/leetcode.png and b/pictures/floodfill/leetcode.png differ diff --git a/pictures/floodfill/ppt1.PNG b/pictures/floodfill/ppt1.PNG index 22046455d0..1667b0f18f 100644 Binary files a/pictures/floodfill/ppt1.PNG and b/pictures/floodfill/ppt1.PNG differ diff --git a/pictures/floodfill/ppt2.PNG b/pictures/floodfill/ppt2.PNG index 28a220d614..58dd272514 100644 Binary files a/pictures/floodfill/ppt2.PNG and b/pictures/floodfill/ppt2.PNG differ diff --git a/pictures/floodfill/ppt3.PNG b/pictures/floodfill/ppt3.PNG index 6957ffe4ae..0f2a1a80f2 100644 Binary files a/pictures/floodfill/ppt3.PNG and b/pictures/floodfill/ppt3.PNG differ diff --git a/pictures/floodfill/ppt4.PNG b/pictures/floodfill/ppt4.PNG index 9ceb6ed044..f90e4b9e6d 100644 Binary files a/pictures/floodfill/ppt4.PNG and b/pictures/floodfill/ppt4.PNG differ diff --git a/pictures/floodfill/ppt5.PNG b/pictures/floodfill/ppt5.PNG index b2583eef11..f6e5cc0b25 100644 Binary files a/pictures/floodfill/ppt5.PNG and b/pictures/floodfill/ppt5.PNG differ diff --git a/pictures/floodfill/xiaoxiaole.jpg b/pictures/floodfill/xiaoxiaole.jpg index fa443f0cda..fcf657ac8d 100644 Binary files a/pictures/floodfill/xiaoxiaole.jpg and b/pictures/floodfill/xiaoxiaole.jpg differ diff --git "a/pictures/floodfill/\346\211\253\351\233\267.png" "b/pictures/floodfill/\346\211\253\351\233\267.png" index 9b04e0392c..d25e1deac1 100644 Binary files "a/pictures/floodfill/\346\211\253\351\233\267.png" and "b/pictures/floodfill/\346\211\253\351\233\267.png" differ diff --git "a/pictures/floodfill/\346\212\240\345\233\276.jpeg" "b/pictures/floodfill/\346\212\240\345\233\276.jpeg" index fa112468c5..66097dd104 100644 Binary files "a/pictures/floodfill/\346\212\240\345\233\276.jpeg" and "b/pictures/floodfill/\346\212\240\345\233\276.jpeg" differ diff --git a/pictures/group.jpg b/pictures/group.jpg new file mode 100644 index 0000000000..6d942cfcc4 Binary files /dev/null and b/pictures/group.jpg differ diff --git a/pictures/header.jpg b/pictures/header.jpg new file mode 100644 index 0000000000..06a2048f45 Binary files /dev/null and b/pictures/header.jpg differ diff --git a/pictures/heap/1.png b/pictures/heap/1.png index 93f4387839..8d99c0d9b1 100644 Binary files a/pictures/heap/1.png and b/pictures/heap/1.png differ diff --git a/pictures/intersection/1.jpg b/pictures/intersection/1.jpg index 03b9cbf67c..97f313fe08 100644 Binary files a/pictures/intersection/1.jpg and b/pictures/intersection/1.jpg differ diff --git a/pictures/intersection/2.jpg b/pictures/intersection/2.jpg index 3434de4d1e..555688e4e7 100644 Binary files a/pictures/intersection/2.jpg and b/pictures/intersection/2.jpg differ diff --git a/pictures/intersection/3.jpg b/pictures/intersection/3.jpg index b5e805df66..9ba0b61d8a 100644 Binary files a/pictures/intersection/3.jpg and b/pictures/intersection/3.jpg differ diff --git a/pictures/intersection/title.png b/pictures/intersection/title.png index 4b0529a808..d06d89249b 100644 Binary files a/pictures/intersection/title.png and b/pictures/intersection/title.png differ diff --git a/pictures/interval/2.jpg b/pictures/interval/2.jpg index aa0440f0e1..06e34bd6a2 100644 Binary files a/pictures/interval/2.jpg and b/pictures/interval/2.jpg differ diff --git a/pictures/interval/3.jpg b/pictures/interval/3.jpg index 89b4e6eff1..68741642ea 100644 Binary files a/pictures/interval/3.jpg and b/pictures/interval/3.jpg differ diff --git a/pictures/interval/4.jpg b/pictures/interval/4.jpg index c6735d83ad..d9fb1f4a19 100644 Binary files a/pictures/interval/4.jpg and b/pictures/interval/4.jpg differ diff --git a/pictures/interval/title1.png b/pictures/interval/title1.png index 551c259902..e90959bad5 100644 Binary files a/pictures/interval/title1.png and b/pictures/interval/title1.png differ diff --git a/pictures/interval/title2.png b/pictures/interval/title2.png index 41261129bc..626d0c5c9b 100644 Binary files a/pictures/interval/title2.png and b/pictures/interval/title2.png differ diff --git a/pictures/kgroup/1.jpg b/pictures/kgroup/1.jpg index ca0c8ff8b4..ab874fcab4 100644 Binary files a/pictures/kgroup/1.jpg and b/pictures/kgroup/1.jpg differ diff --git a/pictures/kgroup/2.jpg b/pictures/kgroup/2.jpg index 653d31f4dc..ca29ffd706 100644 Binary files a/pictures/kgroup/2.jpg and b/pictures/kgroup/2.jpg differ diff --git a/pictures/kgroup/3.jpg b/pictures/kgroup/3.jpg index 8718e51c31..e2255241ec 100644 Binary files a/pictures/kgroup/3.jpg and b/pictures/kgroup/3.jpg differ diff --git a/pictures/kgroup/4.jpg b/pictures/kgroup/4.jpg index f2fed3e33a..6126add251 100644 Binary files a/pictures/kgroup/4.jpg and b/pictures/kgroup/4.jpg differ diff --git a/pictures/kgroup/5.jpg b/pictures/kgroup/5.jpg index 4cd3b33242..2b626b30e4 100644 Binary files a/pictures/kgroup/5.jpg and b/pictures/kgroup/5.jpg differ diff --git a/pictures/kgroup/6.jpg b/pictures/kgroup/6.jpg index 4e1772b0bc..f11088f7cb 100644 Binary files a/pictures/kgroup/6.jpg and b/pictures/kgroup/6.jpg differ diff --git a/pictures/kgroup/7.jpg b/pictures/kgroup/7.jpg index 073ba61f3e..8b4bf79934 100644 Binary files a/pictures/kgroup/7.jpg and b/pictures/kgroup/7.jpg differ diff --git a/pictures/kgroup/title.png b/pictures/kgroup/title.png index 794f5a5cc5..1a2028185e 100644 Binary files a/pictures/kgroup/title.png and b/pictures/kgroup/title.png differ diff --git a/pictures/kmp/1.gif b/pictures/kmp/1.gif index a049ff3456..95732bf8ee 100644 Binary files a/pictures/kmp/1.gif and b/pictures/kmp/1.gif differ diff --git a/pictures/kmp/2.gif b/pictures/kmp/2.gif index 418d02b7bc..ed1059d958 100644 Binary files a/pictures/kmp/2.gif and b/pictures/kmp/2.gif differ diff --git a/pictures/kmp/3.gif b/pictures/kmp/3.gif index 309e95efa0..e91497f4ca 100644 Binary files a/pictures/kmp/3.gif and b/pictures/kmp/3.gif differ diff --git a/pictures/kmp/A.gif b/pictures/kmp/A.gif index 10c48fb569..dd0800e17b 100644 Binary files a/pictures/kmp/A.gif and b/pictures/kmp/A.gif differ diff --git a/pictures/kmp/allstate.jpg b/pictures/kmp/allstate.jpg index bfbd26bcc4..f8e40777d1 100644 Binary files a/pictures/kmp/allstate.jpg and b/pictures/kmp/allstate.jpg differ diff --git a/pictures/kmp/back.jpg b/pictures/kmp/back.jpg index e33cc25cd7..368d091098 100644 Binary files a/pictures/kmp/back.jpg and b/pictures/kmp/back.jpg differ diff --git a/pictures/kmp/dfa.gif b/pictures/kmp/dfa.gif index 0e9bdaa4f8..0f5dc54224 100644 Binary files a/pictures/kmp/dfa.gif and b/pictures/kmp/dfa.gif differ diff --git a/pictures/kmp/exp1.jpg b/pictures/kmp/exp1.jpg index 5bb393523e..9e413d0438 100644 Binary files a/pictures/kmp/exp1.jpg and b/pictures/kmp/exp1.jpg differ diff --git a/pictures/kmp/exp2.jpg b/pictures/kmp/exp2.jpg index f277b7e81b..244fc4ee4b 100644 Binary files a/pictures/kmp/exp2.jpg and b/pictures/kmp/exp2.jpg differ diff --git a/pictures/kmp/exp3.jpg b/pictures/kmp/exp3.jpg index a7e48efb38..11eae5a6d2 100644 Binary files a/pictures/kmp/exp3.jpg and b/pictures/kmp/exp3.jpg differ diff --git a/pictures/kmp/exp4.jpg b/pictures/kmp/exp4.jpg index f7f98ee58c..e969c48d8a 100644 Binary files a/pictures/kmp/exp4.jpg and b/pictures/kmp/exp4.jpg differ diff --git a/pictures/kmp/exp5.jpg b/pictures/kmp/exp5.jpg index 979d91e31d..2a16a868ab 100644 Binary files a/pictures/kmp/exp5.jpg and b/pictures/kmp/exp5.jpg differ diff --git a/pictures/kmp/exp6.jpg b/pictures/kmp/exp6.jpg index 423b244d43..87c723f3b8 100644 Binary files a/pictures/kmp/exp6.jpg and b/pictures/kmp/exp6.jpg differ diff --git a/pictures/kmp/exp7.jpg b/pictures/kmp/exp7.jpg index 10da813832..95634204bd 100644 Binary files a/pictures/kmp/exp7.jpg and b/pictures/kmp/exp7.jpg differ diff --git a/pictures/kmp/forward.jpg b/pictures/kmp/forward.jpg index 66beca0ebe..2f887c8933 100644 Binary files a/pictures/kmp/forward.jpg and b/pictures/kmp/forward.jpg differ diff --git a/pictures/kmp/kmp.gif b/pictures/kmp/kmp.gif index e33dad97b3..18cd52dbd8 100644 Binary files a/pictures/kmp/kmp.gif and b/pictures/kmp/kmp.gif differ diff --git a/pictures/kmp/shadow.jpg b/pictures/kmp/shadow.jpg index 0733b546e2..9b2f536aab 100644 Binary files a/pictures/kmp/shadow.jpg and b/pictures/kmp/shadow.jpg differ diff --git a/pictures/kmp/shadow1.jpg b/pictures/kmp/shadow1.jpg index 13d0377725..07389fc897 100644 Binary files a/pictures/kmp/shadow1.jpg and b/pictures/kmp/shadow1.jpg differ diff --git a/pictures/kmp/shadow2.jpg b/pictures/kmp/shadow2.jpg index a9abb79d7d..f68660afe0 100644 Binary files a/pictures/kmp/shadow2.jpg and b/pictures/kmp/shadow2.jpg differ diff --git a/pictures/kmp/state.jpg b/pictures/kmp/state.jpg index 073060b2bf..3fd084cca8 100644 Binary files a/pictures/kmp/state.jpg and b/pictures/kmp/state.jpg differ diff --git a/pictures/kmp/state2.jpg b/pictures/kmp/state2.jpg index 3be64c091f..f20fde5d12 100644 Binary files a/pictures/kmp/state2.jpg and b/pictures/kmp/state2.jpg differ diff --git a/pictures/kmp/state4.jpg b/pictures/kmp/state4.jpg index d5ba19eaf2..8873fd38be 100644 Binary files a/pictures/kmp/state4.jpg and b/pictures/kmp/state4.jpg differ diff --git a/pictures/kmp/txt1.jpg b/pictures/kmp/txt1.jpg index a123d9e360..3b10191619 100644 Binary files a/pictures/kmp/txt1.jpg and b/pictures/kmp/txt1.jpg differ diff --git a/pictures/kmp/txt2.jpg b/pictures/kmp/txt2.jpg index bbb7cd3781..c6c5041e66 100644 Binary files a/pictures/kmp/txt2.jpg and b/pictures/kmp/txt2.jpg differ diff --git a/pictures/kmp/txt3.jpg b/pictures/kmp/txt3.jpg index e4edfd70ae..29830ba73e 100644 Binary files a/pictures/kmp/txt3.jpg and b/pictures/kmp/txt3.jpg differ diff --git a/pictures/kmp/txt4.jpg b/pictures/kmp/txt4.jpg index 50e36d1fb1..55e53e7719 100644 Binary files a/pictures/kmp/txt4.jpg and b/pictures/kmp/txt4.jpg differ diff --git a/pictures/kmp/z.jpg b/pictures/kmp/z.jpg index aebf0b8609..56b945efcf 100644 Binary files a/pictures/kmp/z.jpg and b/pictures/kmp/z.jpg differ diff --git a/pictures/labuladong.jpg b/pictures/labuladong.jpg deleted file mode 100644 index cb385ab0ad..0000000000 Binary files a/pictures/labuladong.jpg and /dev/null differ diff --git a/pictures/labuladong.png b/pictures/labuladong.png index fe2100acdd..a63a5bd890 100644 Binary files a/pictures/labuladong.png and b/pictures/labuladong.png differ diff --git a/pictures/linux-fs/application.png b/pictures/linux-fs/application.png index 17185b81d5..9b39088f36 100644 Binary files a/pictures/linux-fs/application.png and b/pictures/linux-fs/application.png differ diff --git a/pictures/linux-fs/apt.png b/pictures/linux-fs/apt.png index bed240ba9d..5038c8c267 100644 Binary files a/pictures/linux-fs/apt.png and b/pictures/linux-fs/apt.png differ diff --git a/pictures/linux-fs/bin.png b/pictures/linux-fs/bin.png index 755491a457..353baa7bf8 100644 Binary files a/pictures/linux-fs/bin.png and b/pictures/linux-fs/bin.png differ diff --git a/pictures/linux-fs/boot.png b/pictures/linux-fs/boot.png index 1f1b225007..d5848e8cd5 100644 Binary files a/pictures/linux-fs/boot.png and b/pictures/linux-fs/boot.png differ diff --git a/pictures/linux-fs/cpu.png b/pictures/linux-fs/cpu.png index 721b66cd92..12d949e9d9 100644 Binary files a/pictures/linux-fs/cpu.png and b/pictures/linux-fs/cpu.png differ diff --git a/pictures/linux-fs/desktop.png b/pictures/linux-fs/desktop.png index 4cb1239f70..7a46ecef21 100644 Binary files a/pictures/linux-fs/desktop.png and b/pictures/linux-fs/desktop.png differ diff --git a/pictures/linux-fs/dev.png b/pictures/linux-fs/dev.png index dd892e6538..c9275dc85e 100644 Binary files a/pictures/linux-fs/dev.png and b/pictures/linux-fs/dev.png differ diff --git a/pictures/linux-fs/etc.png b/pictures/linux-fs/etc.png index 3a3d1063ef..5de3a4cf78 100644 Binary files a/pictures/linux-fs/etc.png and b/pictures/linux-fs/etc.png differ diff --git a/pictures/linux-fs/home.png b/pictures/linux-fs/home.png index d35c71bba6..dad402e25a 100644 Binary files a/pictures/linux-fs/home.png and b/pictures/linux-fs/home.png differ diff --git a/pictures/linux-fs/linux-filesystem.png b/pictures/linux-fs/linux-filesystem.png index c43b2922d2..225efb8ab3 100644 Binary files a/pictures/linux-fs/linux-filesystem.png and b/pictures/linux-fs/linux-filesystem.png differ diff --git a/pictures/linux-fs/log.png b/pictures/linux-fs/log.png index a4416e1059..ee40ca0a6e 100644 Binary files a/pictures/linux-fs/log.png and b/pictures/linux-fs/log.png differ diff --git a/pictures/linux-fs/opt.png b/pictures/linux-fs/opt.png index 7b2092f234..b46535ec66 100644 Binary files a/pictures/linux-fs/opt.png and b/pictures/linux-fs/opt.png differ diff --git a/pictures/linux-fs/proc.png b/pictures/linux-fs/proc.png index 06006000f1..0e2820a1b5 100644 Binary files a/pictures/linux-fs/proc.png and b/pictures/linux-fs/proc.png differ diff --git a/pictures/linux-fs/root.png b/pictures/linux-fs/root.png index 00df4f75ec..3ba9697367 100644 Binary files a/pictures/linux-fs/root.png and b/pictures/linux-fs/root.png differ diff --git a/pictures/linux-fs/sbin.png b/pictures/linux-fs/sbin.png index 2fc99208df..907c3a0f3e 100644 Binary files a/pictures/linux-fs/sbin.png and b/pictures/linux-fs/sbin.png differ diff --git a/pictures/linux-fs/tmp.png b/pictures/linux-fs/tmp.png index 361146697d..54ea9dd615 100644 Binary files a/pictures/linux-fs/tmp.png and b/pictures/linux-fs/tmp.png differ diff --git a/pictures/linux-fs/usr.png b/pictures/linux-fs/usr.png index 29e389ce35..0619c44937 100644 Binary files a/pictures/linux-fs/usr.png and b/pictures/linux-fs/usr.png differ diff --git a/pictures/linux-fs/usrbin.png b/pictures/linux-fs/usrbin.png index 7dfd72a84f..e1c5e82f6c 100644 Binary files a/pictures/linux-fs/usrbin.png and b/pictures/linux-fs/usrbin.png differ diff --git a/pictures/linuxProcess/1.jpg b/pictures/linuxProcess/1.jpg index a3b4383b8a..a42365fe73 100644 Binary files a/pictures/linuxProcess/1.jpg and b/pictures/linuxProcess/1.jpg differ diff --git a/pictures/linuxProcess/2.jpg b/pictures/linuxProcess/2.jpg index 821ff340fa..9aa08f1401 100644 Binary files a/pictures/linuxProcess/2.jpg and b/pictures/linuxProcess/2.jpg differ diff --git a/pictures/linuxProcess/3.jpg b/pictures/linuxProcess/3.jpg index d7d3ae97ff..a3800a8f21 100644 Binary files a/pictures/linuxProcess/3.jpg and b/pictures/linuxProcess/3.jpg differ diff --git a/pictures/linuxProcess/4.jpg b/pictures/linuxProcess/4.jpg index 6ada0ef14e..c74fdd3722 100644 Binary files a/pictures/linuxProcess/4.jpg and b/pictures/linuxProcess/4.jpg differ diff --git a/pictures/linuxProcess/5.jpg b/pictures/linuxProcess/5.jpg index c4c51ca759..e7b1f74c1d 100644 Binary files a/pictures/linuxProcess/5.jpg and b/pictures/linuxProcess/5.jpg differ diff --git a/pictures/linuxProcess/6.jpg b/pictures/linuxProcess/6.jpg index c2637d131b..9e36322eba 100644 Binary files a/pictures/linuxProcess/6.jpg and b/pictures/linuxProcess/6.jpg differ diff --git a/pictures/linuxProcess/7.jpg b/pictures/linuxProcess/7.jpg index 6abd83436f..80e86879cb 100644 Binary files a/pictures/linuxProcess/7.jpg and b/pictures/linuxProcess/7.jpg differ diff --git a/pictures/linuxProcess/8.jpg b/pictures/linuxProcess/8.jpg index eafae087d3..2b4cb97101 100644 Binary files a/pictures/linuxProcess/8.jpg and b/pictures/linuxProcess/8.jpg differ diff --git a/pictures/linuxshell/1.png b/pictures/linuxshell/1.png index 0657e4d62f..292baa29d0 100644 Binary files a/pictures/linuxshell/1.png and b/pictures/linuxshell/1.png differ diff --git a/pictures/mergeInterval/1.jpg b/pictures/mergeInterval/1.jpg index 915f33607a..5d8a8dbc2e 100644 Binary files a/pictures/mergeInterval/1.jpg and b/pictures/mergeInterval/1.jpg differ diff --git a/pictures/mergeInterval/2.jpg b/pictures/mergeInterval/2.jpg index 79a8ef7f22..28aabd731a 100644 Binary files a/pictures/mergeInterval/2.jpg and b/pictures/mergeInterval/2.jpg differ diff --git a/pictures/mergeInterval/title.png b/pictures/mergeInterval/title.png index ac1a373946..26e42c14e8 100644 Binary files a/pictures/mergeInterval/title.png and b/pictures/mergeInterval/title.png differ diff --git a/pictures/online/1.png b/pictures/online/1.png index 11da044f4d..68db13bada 100644 Binary files a/pictures/online/1.png and b/pictures/online/1.png differ diff --git a/pictures/online/10.png b/pictures/online/10.png index 63041fc421..ec2095fd15 100644 Binary files a/pictures/online/10.png and b/pictures/online/10.png differ diff --git a/pictures/online/11.png b/pictures/online/11.png index 2aac442a00..87d3368cb5 100644 Binary files a/pictures/online/11.png and b/pictures/online/11.png differ diff --git a/pictures/online/2.png b/pictures/online/2.png index b8e460ac25..de635fa8dd 100644 Binary files a/pictures/online/2.png and b/pictures/online/2.png differ diff --git a/pictures/online/3.png b/pictures/online/3.png index 1f465a69f6..c8868697a9 100644 Binary files a/pictures/online/3.png and b/pictures/online/3.png differ diff --git a/pictures/online/4.png b/pictures/online/4.png index 5d99ec999a..7534e3e76f 100644 Binary files a/pictures/online/4.png and b/pictures/online/4.png differ diff --git a/pictures/online/5.png b/pictures/online/5.png index c118acd3a6..9d9ae0a2d8 100644 Binary files a/pictures/online/5.png and b/pictures/online/5.png differ diff --git a/pictures/online/6.png b/pictures/online/6.png index f77e12a989..06c079fdca 100644 Binary files a/pictures/online/6.png and b/pictures/online/6.png differ diff --git a/pictures/online/7.png b/pictures/online/7.png index a031c195a6..9098f63ad4 100644 Binary files a/pictures/online/7.png and b/pictures/online/7.png differ diff --git a/pictures/online/8.png b/pictures/online/8.png index 0822ef48d9..b462ad6d7c 100644 Binary files a/pictures/online/8.png and b/pictures/online/8.png differ diff --git a/pictures/online/9.png b/pictures/online/9.png index a94900f779..f879e25516 100644 Binary files a/pictures/online/9.png and b/pictures/online/9.png differ diff --git a/pictures/others/leetcode.jpeg b/pictures/others/leetcode.jpeg index 8b2c7c776b..2fd2c684db 100644 Binary files a/pictures/others/leetcode.jpeg and b/pictures/others/leetcode.jpeg differ diff --git a/pictures/pancakeSort/1.jpg b/pictures/pancakeSort/1.jpg index 10f9dd6568..903dec371b 100644 Binary files a/pictures/pancakeSort/1.jpg and b/pictures/pancakeSort/1.jpg differ diff --git a/pictures/pancakeSort/2.png b/pictures/pancakeSort/2.png index e57fd31a48..57e16de2dd 100644 Binary files a/pictures/pancakeSort/2.png and b/pictures/pancakeSort/2.png differ diff --git a/pictures/pancakeSort/3.jpg b/pictures/pancakeSort/3.jpg index c492e8bf8b..b36f6fcc80 100644 Binary files a/pictures/pancakeSort/3.jpg and b/pictures/pancakeSort/3.jpg differ diff --git a/pictures/pancakeSort/4.jpg b/pictures/pancakeSort/4.jpg index 53341a3554..f9245e5937 100644 Binary files a/pictures/pancakeSort/4.jpg and b/pictures/pancakeSort/4.jpg differ diff --git a/pictures/pancakeSort/title.png b/pictures/pancakeSort/title.png index f91a8132ce..76a52a0f38 100644 Binary files a/pictures/pancakeSort/title.png and b/pictures/pancakeSort/title.png differ diff --git a/pictures/pay.jpg b/pictures/pay.jpg index 5322a657db..39e9d402a4 100644 Binary files a/pictures/pay.jpg and b/pictures/pay.jpg differ diff --git a/pictures/plugin/chrome.gif b/pictures/plugin/chrome.gif new file mode 100644 index 0000000000..d3ad03cff9 Binary files /dev/null and b/pictures/plugin/chrome.gif differ diff --git a/pictures/plugin/chrome.jpg b/pictures/plugin/chrome.jpg new file mode 100644 index 0000000000..4f5fc680a4 Binary files /dev/null and b/pictures/plugin/chrome.jpg differ diff --git a/pictures/plugin/jetbrain.gif b/pictures/plugin/jetbrain.gif new file mode 100644 index 0000000000..e2deb6d05c Binary files /dev/null and b/pictures/plugin/jetbrain.gif differ diff --git a/pictures/plugin/jetbrain.jpg b/pictures/plugin/jetbrain.jpg new file mode 100644 index 0000000000..061aee41e4 Binary files /dev/null and b/pictures/plugin/jetbrain.jpg differ diff --git a/pictures/plugin/vscode.gif b/pictures/plugin/vscode.gif new file mode 100644 index 0000000000..a0962b45df Binary files /dev/null and b/pictures/plugin/vscode.gif differ diff --git a/pictures/plugin/vscode.jpg b/pictures/plugin/vscode.jpg new file mode 100644 index 0000000000..3c68f20389 Binary files /dev/null and b/pictures/plugin/vscode.jpg differ diff --git "a/pictures/plugin/\345\205\250\345\256\266\346\241\266.jpg" "b/pictures/plugin/\345\205\250\345\256\266\346\241\266.jpg" new file mode 100644 index 0000000000..b97e4cbb4b Binary files /dev/null and "b/pictures/plugin/\345\205\250\345\256\266\346\241\266.jpg" differ diff --git a/pictures/qrcode.jpg b/pictures/qrcode.jpg index d6beb95c7b..83e46cb25f 100644 Binary files a/pictures/qrcode.jpg and b/pictures/qrcode.jpg differ diff --git "a/pictures/redis\345\205\245\344\276\265/1.png" "b/pictures/redis\345\205\245\344\276\265/1.png" index fb2e59dc41..e021d3eb44 100644 Binary files "a/pictures/redis\345\205\245\344\276\265/1.png" and "b/pictures/redis\345\205\245\344\276\265/1.png" differ diff --git "a/pictures/redis\345\205\245\344\276\265/2.png" "b/pictures/redis\345\205\245\344\276\265/2.png" index 94fcae0456..a9f03e65e9 100644 Binary files "a/pictures/redis\345\205\245\344\276\265/2.png" and "b/pictures/redis\345\205\245\344\276\265/2.png" differ diff --git "a/pictures/redis\345\205\245\344\276\265/3.png" "b/pictures/redis\345\205\245\344\276\265/3.png" index d559c635db..b4c9980e83 100644 Binary files "a/pictures/redis\345\205\245\344\276\265/3.png" and "b/pictures/redis\345\205\245\344\276\265/3.png" differ diff --git "a/pictures/redis\345\205\245\344\276\265/4.png" "b/pictures/redis\345\205\245\344\276\265/4.png" index 2abc2eedcf..44b390d666 100644 Binary files "a/pictures/redis\345\205\245\344\276\265/4.png" and "b/pictures/redis\345\205\245\344\276\265/4.png" differ diff --git "a/pictures/redis\345\205\245\344\276\265/5.png" "b/pictures/redis\345\205\245\344\276\265/5.png" index 2d6c01e14b..60dc5ab738 100644 Binary files "a/pictures/redis\345\205\245\344\276\265/5.png" and "b/pictures/redis\345\205\245\344\276\265/5.png" differ diff --git "a/pictures/redis\345\205\245\344\276\265/6.png" "b/pictures/redis\345\205\245\344\276\265/6.png" index 48cd894f7d..618c28c203 100644 Binary files "a/pictures/redis\345\205\245\344\276\265/6.png" and "b/pictures/redis\345\205\245\344\276\265/6.png" differ diff --git a/pictures/robber/1.jpg b/pictures/robber/1.jpg index 62cec652a6..23b590f6a9 100644 Binary files a/pictures/robber/1.jpg and b/pictures/robber/1.jpg differ diff --git a/pictures/robber/2.jpg b/pictures/robber/2.jpg index 89ffabb077..95761fddbc 100644 Binary files a/pictures/robber/2.jpg and b/pictures/robber/2.jpg differ diff --git a/pictures/robber/3.jpg b/pictures/robber/3.jpg index 35e169e770..6c54da1aa9 100644 Binary files a/pictures/robber/3.jpg and b/pictures/robber/3.jpg differ diff --git a/pictures/robber/title.png b/pictures/robber/title.png index c4bd7822bf..8f60633f9d 100644 Binary files a/pictures/robber/title.png and b/pictures/robber/title.png differ diff --git a/pictures/robber/title1.png b/pictures/robber/title1.png index 736ece0c9f..b6096939dd 100644 Binary files a/pictures/robber/title1.png and b/pictures/robber/title1.png differ diff --git a/pictures/session/1.png b/pictures/session/1.png index 2b7c7b1b9f..906f7090f7 100644 Binary files a/pictures/session/1.png and b/pictures/session/1.png differ diff --git a/pictures/session/2.png b/pictures/session/2.png index 4662883971..d2660b2219 100644 Binary files a/pictures/session/2.png and b/pictures/session/2.png differ diff --git a/pictures/session/3.png b/pictures/session/3.png index 59c12d4988..622a8f707e 100644 Binary files a/pictures/session/3.png and b/pictures/session/3.png differ diff --git a/pictures/session/4.jpg b/pictures/session/4.jpg index a838e406a3..81e92eedd1 100644 Binary files a/pictures/session/4.jpg and b/pictures/session/4.jpg differ diff --git a/pictures/souyisou.png b/pictures/souyisou.png new file mode 100644 index 0000000000..94d9f67c1e Binary files /dev/null and b/pictures/souyisou.png differ diff --git a/pictures/souyisou2.png b/pictures/souyisou2.png new file mode 100644 index 0000000000..744d478f16 Binary files /dev/null and b/pictures/souyisou2.png differ diff --git a/pictures/unionfind/1.jpg b/pictures/unionfind/1.jpg index 7e5f6fb607..77aaf0ee22 100644 Binary files a/pictures/unionfind/1.jpg and b/pictures/unionfind/1.jpg differ diff --git a/pictures/unionfind/2.jpg b/pictures/unionfind/2.jpg index af87343e6f..a5396986e9 100644 Binary files a/pictures/unionfind/2.jpg and b/pictures/unionfind/2.jpg differ diff --git a/pictures/unionfind/3.jpg b/pictures/unionfind/3.jpg index 84a97b403c..d3c6051348 100644 Binary files a/pictures/unionfind/3.jpg and b/pictures/unionfind/3.jpg differ diff --git a/pictures/unionfind/4.jpg b/pictures/unionfind/4.jpg index d967c663c6..4004791778 100644 Binary files a/pictures/unionfind/4.jpg and b/pictures/unionfind/4.jpg differ diff --git a/pictures/unionfind/5.jpg b/pictures/unionfind/5.jpg index aed3cd1574..273302945c 100644 Binary files a/pictures/unionfind/5.jpg and b/pictures/unionfind/5.jpg differ diff --git a/pictures/unionfind/6.jpg b/pictures/unionfind/6.jpg index ca30431e28..ae37f8d0ba 100644 Binary files a/pictures/unionfind/6.jpg and b/pictures/unionfind/6.jpg differ diff --git a/pictures/unionfind/7.jpg b/pictures/unionfind/7.jpg index b9cc6e1347..251dd20f05 100644 Binary files a/pictures/unionfind/7.jpg and b/pictures/unionfind/7.jpg differ diff --git a/pictures/unionfind/8.jpg b/pictures/unionfind/8.jpg index 7a544068b7..fd74212075 100644 Binary files a/pictures/unionfind/8.jpg and b/pictures/unionfind/8.jpg differ diff --git "a/pictures/unionfind\345\272\224\347\224\250/1.jpg" "b/pictures/unionfind\345\272\224\347\224\250/1.jpg" index f3a311a27e..583e66121b 100644 Binary files "a/pictures/unionfind\345\272\224\347\224\250/1.jpg" and "b/pictures/unionfind\345\272\224\347\224\250/1.jpg" differ diff --git "a/pictures/unionfind\345\272\224\347\224\250/2.jpg" "b/pictures/unionfind\345\272\224\347\224\250/2.jpg" index 7c1e6acd50..5e4c40b9b4 100644 Binary files "a/pictures/unionfind\345\272\224\347\224\250/2.jpg" and "b/pictures/unionfind\345\272\224\347\224\250/2.jpg" differ diff --git "a/pictures/unionfind\345\272\224\347\224\250/3.jpg" "b/pictures/unionfind\345\272\224\347\224\250/3.jpg" index dca81a8366..a21dd12d70 100644 Binary files "a/pictures/unionfind\345\272\224\347\224\250/3.jpg" and "b/pictures/unionfind\345\272\224\347\224\250/3.jpg" differ diff --git a/pictures/youtube/1.png b/pictures/youtube/1.png index 3c8c59589a..f7f96878a2 100644 Binary files a/pictures/youtube/1.png and b/pictures/youtube/1.png differ diff --git a/pictures/youtube/1573133096614.jpeg b/pictures/youtube/1573133096614.jpeg index c34c38eeb6..27f85dc3ab 100644 Binary files a/pictures/youtube/1573133096614.jpeg and b/pictures/youtube/1573133096614.jpeg differ diff --git a/pictures/youtube/1573133131308.jpeg b/pictures/youtube/1573133131308.jpeg index 0c75129dc0..c464ef3bec 100644 Binary files a/pictures/youtube/1573133131308.jpeg and b/pictures/youtube/1573133131308.jpeg differ diff --git a/pictures/youtube/2.jpg b/pictures/youtube/2.jpg index 2eeae31b5d..490c1c40be 100644 Binary files a/pictures/youtube/2.jpg and b/pictures/youtube/2.jpg differ diff --git a/pictures/youtube/3.jpg b/pictures/youtube/3.jpg index d1112a6447..7d24d92b33 100644 Binary files a/pictures/youtube/3.jpg and b/pictures/youtube/3.jpg differ diff --git a/pictures/youtube/4.jpg b/pictures/youtube/4.jpg index 0f4cd6b3df..3ea617d104 100644 Binary files a/pictures/youtube/4.jpg and b/pictures/youtube/4.jpg differ diff --git "a/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title1.png" "b/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title1.png" index 410b4e66d5..cb85d759a8 100644 Binary files "a/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title1.png" and "b/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title1.png" differ diff --git "a/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title2.png" "b/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title2.png" index adc29e78c4..6dc47c6c99 100644 Binary files "a/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title2.png" and "b/pictures/\344\272\214\345\210\206\345\272\224\347\224\250/title2.png" differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/1.jpg" "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/1.jpg" new file mode 100644 index 0000000000..da0a55ba88 Binary files /dev/null and "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/1.jpg" differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/2.jpg" "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/2.jpg" new file mode 100644 index 0000000000..6865c24b13 Binary files /dev/null and "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/2.jpg" differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/3.jpg" "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/3.jpg" new file mode 100644 index 0000000000..a360ba8767 Binary files /dev/null and "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/3.jpg" differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/4.jpg" "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/4.jpg" new file mode 100644 index 0000000000..35e46116bf Binary files /dev/null and "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/4.jpg" differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch1.png" "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch1.png" index 911bde51e7..d5a410abcd 100644 Binary files "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch1.png" and "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch1.png" differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch2.png" "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch2.png" index d6ca59b939..abff601aba 100644 Binary files "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch2.png" and "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch2.png" differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/poem.png" "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/poem.png" new file mode 100644 index 0000000000..6262c33549 Binary files /dev/null and "b/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/poem.png" differ diff --git "a/pictures/\344\275\215\346\223\215\344\275\234/1.png" "b/pictures/\344\275\215\346\223\215\344\275\234/1.png" index 89bfd0cef5..c4463610a4 100644 Binary files "a/pictures/\344\275\215\346\223\215\344\275\234/1.png" and "b/pictures/\344\275\215\346\223\215\344\275\234/1.png" differ diff --git "a/pictures/\344\275\215\346\223\215\344\275\234/title.png" "b/pictures/\344\275\215\346\223\215\344\275\234/title.png" index 9b961475cc..953f1c3a86 100644 Binary files "a/pictures/\344\275\215\346\223\215\344\275\234/title.png" and "b/pictures/\344\275\215\346\223\215\344\275\234/title.png" differ diff --git "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/0.jpg" "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/0.jpg" index 3a19e9d3ef..48728e408c 100644 Binary files "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/0.jpg" and "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/0.jpg" differ diff --git "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/1.jpg" "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/1.jpg" index 337378019b..df0cc5fb3a 100644 Binary files "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/1.jpg" and "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/1.jpg" differ diff --git "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/2.jpg" "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/2.jpg" index 5c9d52b608..4e256327c4 100644 Binary files "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/2.jpg" and "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/2.jpg" differ diff --git "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/title.png" "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/title.png" index 771fa0c075..fbd9329141 100644 Binary files "a/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/title.png" and "b/pictures/\344\277\241\345\260\201\345\265\214\345\245\227/title.png" differ diff --git "a/pictures/\345\205\250\345\256\266\346\241\266.jpg" "b/pictures/\345\205\250\345\256\266\346\241\266.jpg" new file mode 100644 index 0000000000..b97e4cbb4b Binary files /dev/null and "b/pictures/\345\205\250\345\256\266\346\241\266.jpg" differ diff --git "a/pictures/\345\211\215\347\274\200\345\222\214/1.jpg" "b/pictures/\345\211\215\347\274\200\345\222\214/1.jpg" index 5c748457ee..3b1c46fbb7 100644 Binary files "a/pictures/\345\211\215\347\274\200\345\222\214/1.jpg" and "b/pictures/\345\211\215\347\274\200\345\222\214/1.jpg" differ diff --git "a/pictures/\345\211\215\347\274\200\345\222\214/2.jpg" "b/pictures/\345\211\215\347\274\200\345\222\214/2.jpg" index b0e9993d39..1306f903f3 100644 Binary files "a/pictures/\345\211\215\347\274\200\345\222\214/2.jpg" and "b/pictures/\345\211\215\347\274\200\345\222\214/2.jpg" differ diff --git "a/pictures/\345\211\215\347\274\200\345\222\214/title.png" "b/pictures/\345\211\215\347\274\200\345\222\214/title.png" index 9e52a72f6a..cb19e8180f 100644 Binary files "a/pictures/\345\211\215\347\274\200\345\222\214/title.png" and "b/pictures/\345\211\215\347\274\200\345\222\214/title.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coindp.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coindp.png" index 0b06a4f426..0d1b8a0a12 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coindp.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coindp.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coinfunc.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coinfunc.png" index a07c92ec8c..26e6460f53 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coinfunc.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/coinfunc.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/cointree.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/cointree.png" index 6a57937cb7..26c978a9c6 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/cointree.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/cointree.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibdp.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibdp.png" index 986b4d21b2..f121f029fd 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibdp.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibdp.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibfunc.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibfunc.png" index a1044f232c..8657eae81b 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibfunc.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibfunc.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibmemo.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibmemo.png" index 536a793923..359d1b0cc5 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibmemo.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibmemo.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibtree.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibtree.png" index dc5bf5732d..b5e6f028ca 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibtree.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/fibtree.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013033.441.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013033.441.png" index a1044f232c..8657eae81b 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013033.441.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013033.441.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013830.397.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013830.397.png" index a07c92ec8c..26e6460f53 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013830.397.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/img_20190514_013830.397.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (1).png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (1).png" index 536a793923..359d1b0cc5 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (1).png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (1).png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (2).png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (2).png" index 986b4d21b2..f121f029fd 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (2).png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (2).png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (3).png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (3).png" index 6a57937cb7..26c978a9c6 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (3).png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (3).png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (4).png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (4).png" index 0b06a4f426..0d1b8a0a12 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (4).png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image (4).png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image.png" index dc5bf5732d..b5e6f028ca 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243/ink-image.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/1.jpg" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/1.jpg" index 6178c1abf4..b4732ffc9d 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/1.jpg" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/1.jpg" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/2.jpg" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/2.jpg" index 229d05bd79..386669df9b 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/2.jpg" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/2.jpg" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/3.jpg" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/3.jpg" index 9ce9c1bdd5..3d543edccc 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/3.jpg" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/3.jpg" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/4.jpg" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/4.jpg" index 6946a5546b..58e2a3a53a 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/4.jpg" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/4.jpg" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/5.jpg" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/5.jpg" index 1d8af7a881..64100c8440 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/5.jpg" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/5.jpg" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/6.jpg" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/6.jpg" index b57a81ecc9..fbd22932b8 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/6.jpg" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/6.jpg" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/coin.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/coin.png" index 2b2bdf99cb..2b99fb680c 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/coin.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/coin.png" differ diff --git "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/fib.png" "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/fib.png" index f122fd8df5..c90a9c6473 100644 Binary files "a/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/fib.png" and "b/pictures/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266/fib.png" differ diff --git "a/pictures/\345\215\225\350\260\203\346\240\210/1.png" "b/pictures/\345\215\225\350\260\203\346\240\210/1.png" index a3729004bb..1a2575d094 100644 Binary files "a/pictures/\345\215\225\350\260\203\346\240\210/1.png" and "b/pictures/\345\215\225\350\260\203\346\240\210/1.png" differ diff --git "a/pictures/\345\215\225\350\260\203\346\240\210/2.png" "b/pictures/\345\215\225\350\260\203\346\240\210/2.png" index bdc7ea6b48..aa6635831b 100644 Binary files "a/pictures/\345\215\225\350\260\203\346\240\210/2.png" and "b/pictures/\345\215\225\350\260\203\346\240\210/2.png" differ diff --git "a/pictures/\345\215\225\350\260\203\346\240\210/3.png" "b/pictures/\345\215\225\350\260\203\346\240\210/3.png" index 71d78b84ea..4d067ce487 100644 Binary files "a/pictures/\345\215\225\350\260\203\346\240\210/3.png" and "b/pictures/\345\215\225\350\260\203\346\240\210/3.png" differ diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/1.png" "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/1.png" index ce6d3f1daa..131253f93e 100644 Binary files "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/1.png" and "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/1.png" differ diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/2.png" "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/2.png" index ab02276bdc..49e658cd4c 100644 Binary files "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/2.png" and "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/2.png" differ diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/3.png" "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/3.png" index 4a8bf8743f..500cf80a8e 100644 Binary files "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/3.png" and "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/3.png" differ diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/title.png" "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/title.png" index 62b1f9d26a..13e2a28f5e 100644 Binary files "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/title.png" and "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/title.png" differ diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/1.png" "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/1.png" index 10426deb7d..965db46ef9 100644 Binary files "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/1.png" and "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/1.png" differ diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/2.png" "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/2.png" index 1f0efb0b7d..5205c28ad1 100644 Binary files "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/2.png" and "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/2.png" differ diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/3.png" "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/3.png" index 1c7a3be316..469f3835f5 100644 Binary files "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/3.png" and "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/3.png" differ diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/4.png" "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/4.png" index 137f39b556..0929ea854d 100644 Binary files "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/4.png" and "b/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/4.png" differ diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/1.png" "b/pictures/\345\217\214\346\214\207\351\222\210/1.png" index a5aa914f25..c1fe49a237 100644 Binary files "a/pictures/\345\217\214\346\214\207\351\222\210/1.png" and "b/pictures/\345\217\214\346\214\207\351\222\210/1.png" differ diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/2.png" "b/pictures/\345\217\214\346\214\207\351\222\210/2.png" index 6c829a2055..7d7bd77193 100644 Binary files "a/pictures/\345\217\214\346\214\207\351\222\210/2.png" and "b/pictures/\345\217\214\346\214\207\351\222\210/2.png" differ diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/3.png" "b/pictures/\345\217\214\346\214\207\351\222\210/3.png" index 5d1f57e8ec..de27d35383 100644 Binary files "a/pictures/\345\217\214\346\214\207\351\222\210/3.png" and "b/pictures/\345\217\214\346\214\207\351\222\210/3.png" differ diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/center.png" "b/pictures/\345\217\214\346\214\207\351\222\210/center.png" index 487902356b..586e6e38fc 100644 Binary files "a/pictures/\345\217\214\346\214\207\351\222\210/center.png" and "b/pictures/\345\217\214\346\214\207\351\222\210/center.png" differ diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/title.png" "b/pictures/\345\217\214\346\214\207\351\222\210/title.png" index 280d516953..ff32a9c7e5 100644 Binary files "a/pictures/\345\217\214\346\214\207\351\222\210/title.png" and "b/pictures/\345\217\214\346\214\207\351\222\210/title.png" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/1.jpg" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/1.jpg" index 151357e5b9..b9fff89cb1 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/1.jpg" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/1.jpg" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/2.jpg" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/2.jpg" index d50918c554..c244744301 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/2.jpg" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/2.jpg" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/3.jpg" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/3.jpg" index ff2be512a6..be3eed15a2 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/3.jpg" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/3.jpg" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/4.jpg" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/4.jpg" index acfb32e827..bc1a5fa325 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/4.jpg" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/4.jpg" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/5.jpg" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/5.jpg" index c3a5537c85..69362da683 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/5.jpg" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/5.jpg" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/6.jpg" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/6.jpg" index a29283ca33..df31967e27 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/6.jpg" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/6.jpg" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/7.jpg" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/7.jpg" index 1faecf7542..f962eeb29e 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/7.jpg" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/7.jpg" differ diff --git "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/title.png" "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/title.png" index 5a411d6c50..915c1a33d1 100644 Binary files "a/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/title.png" and "b/pictures/\345\217\215\350\275\254\351\223\276\350\241\250/title.png" differ diff --git "a/pictures/\345\233\236\346\226\207/title.png" "b/pictures/\345\233\236\346\226\207/title.png" index 5c6e77d9e6..3ec4774981 100644 Binary files "a/pictures/\345\233\236\346\226\207/title.png" and "b/pictures/\345\233\236\346\226\207/title.png" differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.jpg" "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.jpg" index be6f1d165f..e3f9643f57 100644 Binary files "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.jpg" and "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.jpg" differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/2.jpg" "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/2.jpg" index 217a7c9f4c..828ec0ae41 100644 Binary files "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/2.jpg" and "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/2.jpg" differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/3.jpg" "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/3.jpg" index fbe86512b8..b5162e61ba 100644 Binary files "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/3.jpg" and "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/3.jpg" differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/4.jpg" "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/4.jpg" index 47589f384f..679314382f 100644 Binary files "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/4.jpg" and "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/4.jpg" differ diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/1.jpg" "b/pictures/\345\255\220\345\272\217\345\210\227/1.jpg" index a936d379e3..580a9bcc9b 100644 Binary files "a/pictures/\345\255\220\345\272\217\345\210\227/1.jpg" and "b/pictures/\345\255\220\345\272\217\345\210\227/1.jpg" differ diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/2.jpg" "b/pictures/\345\255\220\345\272\217\345\210\227/2.jpg" index 21a0244d0b..92993c52d1 100644 Binary files "a/pictures/\345\255\220\345\272\217\345\210\227/2.jpg" and "b/pictures/\345\255\220\345\272\217\345\210\227/2.jpg" differ diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/3.jpg" "b/pictures/\345\255\220\345\272\217\345\210\227/3.jpg" index b46eb2f158..b54b6cdd81 100644 Binary files "a/pictures/\345\255\220\345\272\217\345\210\227/3.jpg" and "b/pictures/\345\255\220\345\272\217\345\210\227/3.jpg" differ diff --git "a/pictures/\345\255\220\351\233\206/1.jpg" "b/pictures/\345\255\220\351\233\206/1.jpg" index af13075733..85e712e3a3 100644 Binary files "a/pictures/\345\255\220\351\233\206/1.jpg" and "b/pictures/\345\255\220\351\233\206/1.jpg" differ diff --git "a/pictures/\345\255\220\351\233\206/2.jpg" "b/pictures/\345\255\220\351\233\206/2.jpg" index 8a0341aa1e..b02026d8f2 100644 Binary files "a/pictures/\345\255\220\351\233\206/2.jpg" and "b/pictures/\345\255\220\351\233\206/2.jpg" differ diff --git "a/pictures/\345\255\220\351\233\206/3.jpg" "b/pictures/\345\255\220\351\233\206/3.jpg" index e9ad4c34f5..0b34ef306f 100644 Binary files "a/pictures/\345\255\220\351\233\206/3.jpg" and "b/pictures/\345\255\220\351\233\206/3.jpg" differ diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/1.jpg" "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/1.jpg" index 01f78f5fac..d771001d86 100644 Binary files "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/1.jpg" and "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/1.jpg" differ diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/2.jpg" "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/2.jpg" index c71e96dbf5..0e3f07f896 100644 Binary files "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/2.jpg" and "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/2.jpg" differ diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/3.jpg" "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/3.jpg" index 6e3fa2e6b2..9fd5077199 100644 Binary files "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/3.jpg" and "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/3.jpg" differ diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/6.jpg" "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/6.jpg" index 221176e91e..b873dd7a50 100644 Binary files "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/6.jpg" and "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/6.jpg" differ diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/title.png" "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/title.png" index 7866cff6f1..e9cf410f06 100644 Binary files "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/title.png" and "b/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/title.png" differ diff --git "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/1.jpg" "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/1.jpg" index 1770e44e90..dacbb65bd2 100644 Binary files "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/1.jpg" and "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/1.jpg" differ diff --git "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/2.jpg" "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/2.jpg" index 16086b9803..e6d28f0ef1 100644 Binary files "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/2.jpg" and "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/2.jpg" differ diff --git "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/3.jpg" "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/3.jpg" index 8ab2b8f11c..b5fc5e217c 100644 Binary files "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/3.jpg" and "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/3.jpg" differ diff --git "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/4.jpg" "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/4.jpg" index 0c86ab9765..09989d4013 100644 Binary files "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/4.jpg" and "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/4.jpg" differ diff --git "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/5.jpg" "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/5.jpg" index e5cd2b74d9..27b67d7c43 100644 Binary files "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/5.jpg" and "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/5.jpg" differ diff --git "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/6.jpg" "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/6.jpg" index e78fa6f9eb..e99f74dfb8 100644 Binary files "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/6.jpg" and "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/6.jpg" differ diff --git "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/7.jpg" "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/7.jpg" index 76dced4d54..a368d636db 100644 Binary files "a/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/7.jpg" and "b/pictures/\345\257\206\347\240\201\346\212\200\346\234\257/7.jpg" differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/1.jpg" "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/1.jpg" index 6971d39a64..a0c943df93 100644 Binary files "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/1.jpg" and "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/1.jpg" differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/2.jpg" "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/2.jpg" index 3025b3d386..b0d46a2c8d 100644 Binary files "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/2.jpg" and "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/2.jpg" differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/3.jpg" "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/3.jpg" index 4eaa1c7c3b..159b32bfdd 100644 Binary files "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/3.jpg" and "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/3.jpg" differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/4.jpg" "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/4.jpg" index 96fb8b964c..1eb30ff5c9 100644 Binary files "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/4.jpg" and "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/4.jpg" differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/5.jpg" "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/5.jpg" index 0ead3214e1..f88315e344 100644 Binary files "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/5.jpg" and "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/5.jpg" differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/6.jpg" "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/6.jpg" index c9ce5a42a9..1095324265 100644 Binary files "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/6.jpg" and "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/6.jpg" differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/7.jpg" "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/7.jpg" index e4c2fbc0e4..0c22c5375c 100644 Binary files "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/7.jpg" and "b/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/7.jpg" differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/1.jpg" "b/pictures/\346\211\224\351\270\241\350\233\213/1.jpg" index d711bb29af..0bc912961b 100644 Binary files "a/pictures/\346\211\224\351\270\241\350\233\213/1.jpg" and "b/pictures/\346\211\224\351\270\241\350\233\213/1.jpg" differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/2.jpg" "b/pictures/\346\211\224\351\270\241\350\233\213/2.jpg" index 72748efb91..5f7f9edf58 100644 Binary files "a/pictures/\346\211\224\351\270\241\350\233\213/2.jpg" and "b/pictures/\346\211\224\351\270\241\350\233\213/2.jpg" differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/3.jpg" "b/pictures/\346\211\224\351\270\241\350\233\213/3.jpg" index 5ba41f1e4f..069d53cfb2 100644 Binary files "a/pictures/\346\211\224\351\270\241\350\233\213/3.jpg" and "b/pictures/\346\211\224\351\270\241\350\233\213/3.jpg" differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/dp.png" "b/pictures/\346\211\224\351\270\241\350\233\213/dp.png" index aa8ff334c8..70169d3d52 100644 Binary files "a/pictures/\346\211\224\351\270\241\350\233\213/dp.png" and "b/pictures/\346\211\224\351\270\241\350\233\213/dp.png" differ diff --git "a/pictures/\346\216\245\351\233\250\346\260\264/0.jpg" "b/pictures/\346\216\245\351\233\250\346\260\264/0.jpg" index 2d2e1cb645..ec17d57f58 100644 Binary files "a/pictures/\346\216\245\351\233\250\346\260\264/0.jpg" and "b/pictures/\346\216\245\351\233\250\346\260\264/0.jpg" differ diff --git "a/pictures/\346\216\245\351\233\250\346\260\264/1.jpg" "b/pictures/\346\216\245\351\233\250\346\260\264/1.jpg" index 6e5196cadf..03e5217171 100644 Binary files "a/pictures/\346\216\245\351\233\250\346\260\264/1.jpg" and "b/pictures/\346\216\245\351\233\250\346\260\264/1.jpg" differ diff --git "a/pictures/\346\216\245\351\233\250\346\260\264/2.jpg" "b/pictures/\346\216\245\351\233\250\346\260\264/2.jpg" index 5aa7684553..839a8bf812 100644 Binary files "a/pictures/\346\216\245\351\233\250\346\260\264/2.jpg" and "b/pictures/\346\216\245\351\233\250\346\260\264/2.jpg" differ diff --git "a/pictures/\346\216\245\351\233\250\346\260\264/3.jpg" "b/pictures/\346\216\245\351\233\250\346\260\264/3.jpg" index 5f74da29ac..a48dc5afcc 100644 Binary files "a/pictures/\346\216\245\351\233\250\346\260\264/3.jpg" and "b/pictures/\346\216\245\351\233\250\346\260\264/3.jpg" differ diff --git "a/pictures/\346\216\245\351\233\250\346\260\264/4.jpg" "b/pictures/\346\216\245\351\233\250\346\260\264/4.jpg" index 953add1171..f97488589e 100644 Binary files "a/pictures/\346\216\245\351\233\250\346\260\264/4.jpg" and "b/pictures/\346\216\245\351\233\250\346\260\264/4.jpg" differ diff --git "a/pictures/\346\216\245\351\233\250\346\260\264/5.jpg" "b/pictures/\346\216\245\351\233\250\346\260\264/5.jpg" index 9d53111988..718509a03f 100644 Binary files "a/pictures/\346\216\245\351\233\250\346\260\264/5.jpg" and "b/pictures/\346\216\245\351\233\250\346\260\264/5.jpg" differ diff --git "a/pictures/\346\216\245\351\233\250\346\260\264/title.png" "b/pictures/\346\216\245\351\233\250\346\260\264/title.png" index f4c908625f..177355ac2b 100644 Binary files "a/pictures/\346\216\245\351\233\250\346\260\264/title.png" and "b/pictures/\346\216\245\351\233\250\346\260\264/title.png" differ diff --git "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (1).png" "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (1).png" index 650f77d1fe..db21abcfb6 100644 Binary files "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (1).png" and "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (1).png" differ diff --git "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (2).png" "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (2).png" index 4ceccb84f1..213ad2a38a 100644 Binary files "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (2).png" and "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (2).png" differ diff --git "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (3).png" "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (3).png" index 931f98e23b..1c9989acf3 100644 Binary files "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (3).png" and "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image (3).png" differ diff --git "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image.png" "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image.png" index 0fda0bf947..e1a1afa3fc 100644 Binary files "a/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image.png" and "b/pictures/\346\225\260\347\273\204\344\272\244\346\215\242/ink-image.png" differ diff --git "a/pictures/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204/1.jpg" "b/pictures/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204/1.jpg" index e20dafac67..a8cc3f4cbe 100644 Binary files "a/pictures/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204/1.jpg" and "b/pictures/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204/1.jpg" differ diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/1.jpg" "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/1.jpg" index c15af92ce7..7e3281a560 100644 Binary files "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/1.jpg" and "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/1.jpg" differ diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/2.jpg" "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/2.jpg" index 541665ce23..9f0baa5a8d 100644 Binary files "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/2.jpg" and "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/2.jpg" differ diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/3.jpg" "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/3.jpg" index ac73079c07..aafb4d028e 100644 Binary files "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/3.jpg" and "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/3.jpg" differ diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/4.jpg" "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/4.jpg" index e78e87866e..843ffacd36 100644 Binary files "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/4.jpg" and "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/4.jpg" differ diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/5.jpg" "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/5.jpg" index d9211a9b39..1dc29efc2b 100644 Binary files "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/5.jpg" and "b/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/5.jpg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/1.jpeg" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/1.jpeg" index 6af4e101f6..3640ab127d 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/1.jpeg" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/1.jpeg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/2.jpeg" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/2.jpeg" index 8dbbbe94c7..c96ed6399b 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/2.jpeg" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/2.jpeg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/3.jpeg" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/3.jpeg" index 5cb141327d..4490d13fdd 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/3.jpeg" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/3.jpeg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker1.jpeg" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker1.jpeg" index 1560d39ebd..093306bee4 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker1.jpeg" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker1.jpeg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker2.jpeg" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker2.jpeg" index e4e5bd433e..2ad0c604ab 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker2.jpeg" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker2.jpeg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker3.jpeg" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker3.jpeg" index c1b979816d..efa99bd717 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker3.jpeg" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker3.jpeg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker4.jpeg" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker4.jpeg" index 429a23120c..ba781198ca 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker4.jpeg" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/poker4.jpeg" differ diff --git "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/title.png" "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/title.png" index 820075da24..65c3aa9222 100644 Binary files "a/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/title.png" and "b/pictures/\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227/title.png" differ diff --git "a/pictures/\346\234\211\345\272\217\346\225\260\347\273\204\345\216\273\351\207\215/title.png" "b/pictures/\346\234\211\345\272\217\346\225\260\347\273\204\345\216\273\351\207\215/title.png" index 47c0ab0faf..5b986b9d0d 100644 Binary files "a/pictures/\346\234\211\345\272\217\346\225\260\347\273\204\345\216\273\351\207\215/title.png" and "b/pictures/\346\234\211\345\272\217\346\225\260\347\273\204\345\216\273\351\207\215/title.png" differ diff --git "a/pictures/\346\240\210\351\230\237\345\210\227/1.jpg" "b/pictures/\346\240\210\351\230\237\345\210\227/1.jpg" index c2788a7ffe..cb72cc5f72 100644 Binary files "a/pictures/\346\240\210\351\230\237\345\210\227/1.jpg" and "b/pictures/\346\240\210\351\230\237\345\210\227/1.jpg" differ diff --git "a/pictures/\346\240\210\351\230\237\345\210\227/2.jpg" "b/pictures/\346\240\210\351\230\237\345\210\227/2.jpg" index 6eafa08bdd..982c344fd3 100644 Binary files "a/pictures/\346\240\210\351\230\237\345\210\227/2.jpg" and "b/pictures/\346\240\210\351\230\237\345\210\227/2.jpg" differ diff --git "a/pictures/\346\240\210\351\230\237\345\210\227/3.jpg" "b/pictures/\346\240\210\351\230\237\345\210\227/3.jpg" index 1b59b8d2c4..ae9a8ef8d7 100644 Binary files "a/pictures/\346\240\210\351\230\237\345\210\227/3.jpg" and "b/pictures/\346\240\210\351\230\237\345\210\227/3.jpg" differ diff --git "a/pictures/\346\240\210\351\230\237\345\210\227/4.jpg" "b/pictures/\346\240\210\351\230\237\345\210\227/4.jpg" index 7d2c558b5b..e29c85ea55 100644 Binary files "a/pictures/\346\240\210\351\230\237\345\210\227/4.jpg" and "b/pictures/\346\240\210\351\230\237\345\210\227/4.jpg" differ diff --git "a/pictures/\346\240\210\351\230\237\345\210\227/5.jpg" "b/pictures/\346\240\210\351\230\237\345\210\227/5.jpg" index 5b47d0eb82..d19d22355b 100644 Binary files "a/pictures/\346\240\210\351\230\237\345\210\227/5.jpg" and "b/pictures/\346\240\210\351\230\237\345\210\227/5.jpg" differ diff --git "a/pictures/\346\240\210\351\230\237\345\210\227/6.jpg" "b/pictures/\346\240\210\351\230\237\345\210\227/6.jpg" index 60a6f2d042..891301d6ef 100644 Binary files "a/pictures/\346\240\210\351\230\237\345\210\227/6.jpg" and "b/pictures/\346\240\210\351\230\237\345\210\227/6.jpg" differ diff --git "a/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/p.png" "b/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/p.png" index 794f5a5cc5..959c02bb27 100644 Binary files "a/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/p.png" and "b/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/p.png" differ diff --git "a/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/sanmen.png" "b/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/sanmen.png" index daf212d629..121a3cfd14 100644 Binary files "a/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/sanmen.png" and "b/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/sanmen.png" differ diff --git "a/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/tree.png" "b/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/tree.png" index 0b59308f5b..98c51178da 100644 Binary files "a/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/tree.png" and "b/pictures/\346\246\202\347\216\207\351\227\256\351\242\230/tree.png" differ diff --git "a/pictures/\346\255\243\345\210\231/1.jpeg" "b/pictures/\346\255\243\345\210\231/1.jpeg" new file mode 100644 index 0000000000..e5977fcae6 Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/1.jpeg" differ diff --git "a/pictures/\346\255\243\345\210\231/2.jpeg" "b/pictures/\346\255\243\345\210\231/2.jpeg" new file mode 100644 index 0000000000..f3fa3711a5 Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/2.jpeg" differ diff --git "a/pictures/\346\255\243\345\210\231/3.jpeg" "b/pictures/\346\255\243\345\210\231/3.jpeg" new file mode 100644 index 0000000000..11d9838ddc Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/3.jpeg" differ diff --git "a/pictures/\346\255\243\345\210\231/4.jpeg" "b/pictures/\346\255\243\345\210\231/4.jpeg" new file mode 100644 index 0000000000..24de168927 Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/4.jpeg" differ diff --git "a/pictures/\346\255\243\345\210\231/example.png" "b/pictures/\346\255\243\345\210\231/example.png" index 3646b16793..9eaace9963 100644 Binary files "a/pictures/\346\255\243\345\210\231/example.png" and "b/pictures/\346\255\243\345\210\231/example.png" differ diff --git "a/pictures/\346\255\243\345\210\231/title.png" "b/pictures/\346\255\243\345\210\231/title.png" index 2423900773..6debfa67d6 100644 Binary files "a/pictures/\346\255\243\345\210\231/title.png" and "b/pictures/\346\255\243\345\210\231/title.png" differ diff --git "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/1.png" "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/1.png" index d85e0093d3..1414c8c4dc 100644 Binary files "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/1.png" and "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/1.png" differ diff --git "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/2.png" "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/2.png" index 0536a15701..9f769de386 100644 Binary files "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/2.png" and "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/2.png" differ diff --git "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/3.png" "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/3.png" index d9fbe24d5d..157d082613 100644 Binary files "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/3.png" and "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/3.png" differ diff --git "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/4.png" "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/4.png" index 0547caaeeb..06f0c04092 100644 Binary files "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/4.png" and "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/4.png" differ diff --git "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/5.jpg" "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/5.jpg" index af797bacc6..0a1f94a022 100644 Binary files "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/5.jpg" and "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/5.jpg" differ diff --git "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/6.png" "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/6.png" index e6be333570..44c2c35a80 100644 Binary files "a/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/6.png" and "b/pictures/\346\264\227\347\211\214\347\256\227\346\263\225/6.png" differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/0.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/0.png" index ad4ff28c15..974e71fc83 100644 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/0.png" and "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/0.png" differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/1.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/1.png" index 0825d34608..2967070a4a 100644 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/1.png" and "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/1.png" differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/2.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/2.png" index 04d583c605..95bfa5c2fe 100644 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/2.png" and "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/2.png" differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/3.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/3.png" index e07f12455e..65b5510a3e 100644 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/3.png" and "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/3.png" differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title1.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title1.png" index c84081f9c5..3d5d2bc76c 100644 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title1.png" and "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title1.png" differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title2.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title2.png" index cb33c71545..ce357d5db2 100644 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title2.png" and "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title2.png" differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title3.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title3.png" index 9a1ad10542..0de1770c3f 100644 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title3.png" and "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title3.png" differ diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/1.jpg" "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/1.jpg" index 7bbc566938..180b57081a 100644 Binary files "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/1.jpg" and "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/1.jpg" differ diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/2.jpg" "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/2.jpg" index 7458897d9f..44e1b8076e 100644 Binary files "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/2.jpg" and "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/2.jpg" differ diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/3.jpg" "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/3.jpg" index 6114a5fc3a..bec62f3fff 100644 Binary files "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/3.jpg" and "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/3.jpg" differ diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/title.png" "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/title.png" index 18065d570d..4b2eff0e45 100644 Binary files "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/title.png" and "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/title.png" differ diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/xor.png" "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/xor.png" index 20d360007c..1cd5e85a6f 100644 Binary files "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/xor.png" and "b/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/xor.png" differ diff --git "a/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/1.png" "b/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/1.png" index db1b56a9ef..298b497197 100644 Binary files "a/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/1.png" and "b/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/1.png" differ diff --git "a/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/title.png" "b/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/title.png" index 44406abe31..f2f9779541 100644 Binary files "a/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/title.png" and "b/pictures/\350\202\241\347\245\250\351\227\256\351\242\230/title.png" differ diff --git "a/pictures/\350\256\276\350\256\241Twitter/design.png" "b/pictures/\350\256\276\350\256\241Twitter/design.png" index 623b58242b..2427a47e3f 100644 Binary files "a/pictures/\350\256\276\350\256\241Twitter/design.png" and "b/pictures/\350\256\276\350\256\241Twitter/design.png" differ diff --git "a/pictures/\350\256\276\350\256\241Twitter/tweet.jpg" "b/pictures/\350\256\276\350\256\241Twitter/tweet.jpg" index 89b5bfe2c1..cee1199abd 100644 Binary files "a/pictures/\350\256\276\350\256\241Twitter/tweet.jpg" and "b/pictures/\350\256\276\350\256\241Twitter/tweet.jpg" differ diff --git "a/pictures/\350\256\276\350\256\241Twitter/user.jpg" "b/pictures/\350\256\276\350\256\241Twitter/user.jpg" index 0471ec44b6..dce6b67e0a 100644 Binary files "a/pictures/\350\256\276\350\256\241Twitter/user.jpg" and "b/pictures/\350\256\276\350\256\241Twitter/user.jpg" differ diff --git a/starHistory.jpg b/starHistory.jpg new file mode 100644 index 0000000000..2847906b4b Binary files /dev/null and b/starHistory.jpg differ diff --git a/starHistory.png b/starHistory.png new file mode 100644 index 0000000000..340e80263c Binary files /dev/null and b/starHistory.png differ diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/LCS.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/LCS.md" new file mode 100644 index 0000000000..253d6f1239 --- /dev/null +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/LCS.md" @@ -0,0 +1,408 @@ +# 经典动态规划:最长公共子序列 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1143. Longest Common Subsequence](https://leetcode.com/problems/longest-common-subsequence/) | [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) | 🟠 | +| [583. Delete Operation for Two Strings](https://leetcode.com/problems/delete-operation-for-two-strings/) | [583. 两个字符串的删除操作](https://leetcode.cn/problems/delete-operation-for-two-strings/) | 🟠 | +| [712. Minimum ASCII Delete Sum for Two Strings](https://leetcode.com/problems/minimum-ascii-delete-sum-for-two-strings/) | [712. 两个字符串的最小ASCII删除和](https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +不知道大家做算法题有什么感觉,我总结出来做算法题的技巧就是,把大的问题细化到一个点,先研究在这个小的点上如何解决问题,然后再通过递归/迭代的方式扩展到整个问题。 + +比如说我们前文 [手把手带你刷二叉树第三期](https://labuladong.online/algo/data-structure/binary-tree-part3/),解决二叉树的题目,我们就会把整个问题细化到某一个节点上,想象自己站在某个节点上,需要做什么,然后套二叉树递归框架就行了。 + +动态规划系列问题也是一样,尤其是子序列相关的问题。**本文从「最长公共子序列问题」展开,总结三道子序列问题**,解这道题仔细讲讲这种子序列问题的套路,你就能感受到这种思维方式了。 + +## 最长公共子序列 + +计算最长公共子序列(Longest Common Subsequence,简称 LCS)是一道经典的动态规划题目,力扣第 1143 题「最长公共子序列」就是这个问题: + +给你输入两个字符串 `s1` 和 `s2`,请你找出他们俩的最长公共子序列,返回这个子序列的长度。函数签名如下: + +```java +int longestCommonSubsequence(String s1, String s2); +``` + +比如说输入 `s1 = "zabcde", s2 = "acez"`,它俩的最长公共子序列是 `lcs = "ace"`,长度为 3,所以算法返回 3。 + +如果没有做过这道题,一个最简单的暴力算法就是,把 `s1` 和 `s2` 的所有子序列都穷举出来,然后看看有没有公共的,然后在所有公共子序列里面再寻找一个长度最大的。 + +显然,这种思路的复杂度非常高,你要穷举出所有子序列,这个复杂度就是指数级的,肯定不实际。 + +正确的思路是不要考虑整个字符串,而是细化到 `s1` 和 `s2` 的每个字符。前文 [子序列解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) 中总结的一个规律: + + + + + + + +**对于两个字符串求子序列的问题,都是用两个指针 `i` 和 `j` 分别在两个字符串上移动,大概率是动态规划思路**。 + +最长公共子序列的问题也可以遵循这个规律,我们可以先写一个 `dp` 函数: + +```java +// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度 +int dp(String s1, int i, String s2, int j) +``` + +根据这个 `dp` 函数的定义,那么我们想要的答案就是 `dp(s1, 0, s2, 0)`,且 base case 就是 `i == len(s1)` 或 `j == len(s2)` 时,因为这时候 `s1[i..]` 或 `s2[j..]` 就相当于空串了,最长公共子序列的长度显然是 0: + +```java +int longestCommonSubsequence(String s1, String s2) { + return dp(s1, 0, s2, 0); +} + +// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度 +int dp(String s1, int i, String s2, int j) { + // base case + if (i == s1.length() || j == s2.length()) { + return 0; + } + // ... +} +``` + +**接下来,咱不要看 `s1` 和 `s2` 两个字符串,而是要具体到每一个字符,思考每个字符该做什么**。 + +![](https://labuladong.online/algo/images/LCS/1.jpeg) + +我们只看 `s1[i]` 和 `s2[j]`,**如果 `s1[i] == s2[j]`,说明这个字符一定在 `lcs` 中**: + +![](https://labuladong.online/algo/images/LCS/2.jpeg) + +这样,就找到了一个 `lcs` 中的字符,根据 `dp` 函数的定义,我们可以完善一下代码: + +```java +// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度 +int dp(String s1, int i, String s2, int j) { + if (s1.charAt(i) == s2.charAt(j)) { + // s1[i] 和 s2[j] 必然在 lcs 中, + // 加上 s1[i+1..] 和 s2[j+1..] 中的 lcs 长度,就是答案 + return 1 + dp(s1, i + 1, s2, j + 1); + } else { + // ... + } +} +``` + +刚才说的 `s1[i] == s2[j]` 的情况,但如果 `s1[i] != s2[j]`,应该怎么办呢? + +**`s1[i] != s2[j]` 意味着,`s1[i]` 和 `s2[j]` 中至少有一个字符不在 `lcs` 中**: + +![](https://labuladong.online/algo/images/LCS/3.jpeg) + +如上图,总共可能有三种情况,我怎么知道具体是那种情况呢? + +其实我们也不知道,那就把这三种情况的答案都算出来,取其中结果最大的那个呗,因为题目让我们算「最长」公共子序列的长度嘛。 + +这三种情况的答案怎么算?回想一下我们的 `dp` 函数定义,不就是专门为了计算它们而设计的嘛! + +代码可以再进一步: + +```java +// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度 +int dp(String s1, int i, String s2, int j) { + if (s1.charAt(i) == s2.charAt(j)) { + return 1 + dp(s1, i + 1, s2, j + 1); + } else { + // s1[i] 和 s2[j] 中至少有一个字符不在 lcs 中, + // 穷举三种情况的结果,取其中的最大结果 + return max( + // 情况一、s1[i] 不在 lcs 中 + dp(s1, i + 1, s2, j), + // 情况二、s2[j] 不在 lcs 中 + dp(s1, i, s2, j + 1), + // 情况三、都不在 lcs 中 + dp(s1, i + 1, s2, j + 1) + ); + } +} +``` + +这里就已经非常接近我们的最终答案了,**还有一个小的优化,情况三「`s1[i]` 和 `s2[j]` 都不在 lcs 中」其实可以直接忽略**。 + +因为我们在求最大值嘛,情况三在计算 `s1[i+1..]` 和 `s2[j+1..]` 的 `lcs` 长度,这个长度肯定是小于等于情况二 `s1[i..]` 和 `s2[j+1..]` 中的 `lcs` 长度的,因为 `s1[i+1..]` 比 `s1[i..]` 短嘛,那从这里面算出的 `lcs` 当然也不可能更长嘛。 + +同理,情况三的结果肯定也小于等于情况一。**说白了,情况三被情况一和情况二包含了**,所以我们可以直接忽略掉情况三,完整代码如下: + +```java +class Solution { + // 备忘录,消除重叠子问题 + int[][] memo; + + // 主函数 + public int longestCommonSubsequence(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 备忘录值为 -1 代表未曾计算 + memo = new int[m][n]; + for (int[] row : memo) + Arrays.fill(row, -1); + // 计算 s1[0..] 和 s2[0..] 的 lcs 长度 + return dp(s1, 0, s2, 0); + } + + // 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度 + int dp(String s1, int i, String s2, int j) { + // base case + if (i == s1.length() || j == s2.length()) { + return 0; + } + // 如果之前计算过,则直接返回备忘录中的答案 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 根据 s1[i] 和 s2[j] 的情况做选择 + if (s1.charAt(i) == s2.charAt(j)) { + // s1[i] 和 s2[j] 必然在 lcs 中 + memo[i][j] = 1 + dp(s1, i + 1, s2, j + 1); + } else { + // s1[i] 和 s2[j] 至少有一个不在 lcs 中 + memo[i][j] = Math.max( + dp(s1, i + 1, s2, j), + dp(s1, i, s2, j + 1) + ); + } + return memo[i][j]; + } +} +``` + +以上思路完全就是按照我们之前的爆文 [动态规划套路框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 来的,应该是很容易理解的。至于为什么要加 `memo` 备忘录,我们之前写过很多次,为了照顾新来的读者,这里再简单重复一下,首先抽象出我们核心 `dp` 函数的递归框架: + +```java +int dp(int i, int j) { + dp(i + 1, j + 1); // #1 + dp(i, j + 1); // #2 + dp(i + 1, j); // #3 +} +``` + +你看,假设我想从 `dp(i, j)` 转移到 `dp(i+1, j+1)`,有不止一种方式,可以直接走 `#1`,也可以走 `#2 -> #3`,也可以走 `#3 -> #2`。 + +这就是重叠子问题,如果我们不用 `memo` 备忘录消除子问题,那么 `dp(i+1, j+1)` 就会被多次计算,这是没有必要的。 + +至此,最长公共子序列问题就完全解决了,用的是自顶向下带备忘录的动态规划思路,我们当然也可以使用自底向上的迭代的动态规划思路,和我们的递归思路一样,关键是如何定义 `dp` 数组,我这里也写一下自底向上的解法吧: + +```java +class Solution { + public int longestCommonSubsequence(String s1, String s2) { + int m = s1.length(), n = s2.length(); + int[][] dp = new int[m + 1][n + 1]; + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + // base case: dp[0][..] = dp[..][0] = 0 + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + + return dp[m][n]; + } +} +``` + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +自底向上的解法中 `dp` 数组定义的方式和我们的递归解法有一点差异,而且由于数组索引从 0 开始,有索引偏移,不过思路和我们的递归解法完全相同,如果你看懂了递归解法,这个解法应该不难理解。 + +另外,自底向上的解法可以通过我们前文讲过的 [动态规划空间压缩技巧](https://labuladong.online/algo/dynamic-programming/space-optimization/) 来进行优化,把空间复杂度压缩为 O(N),这里由于篇幅所限,就不展开了。 + +下面,来看两道和最长公共子序列相似的两道题目。 + +## 字符串的删除操作 + +这是力扣第 583 题「两个字符串的删除操作」,看下题目: + +给定两个单词 `s1` 和 `s2` ,返回使得 `s1` 和 `s2` 相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。 + +函数签名如下: + +```java +int minDistance(String s1, String s2); +``` + +比如输入 `s1 = "sea" s2 = "eat"`,算法返回 2,第一步将 `"sea"` 变为 `"ea"` ,第二步将 `"eat"` 变为 `"ea"`。 + +题目让我们计算将两个字符串变得相同的最少删除次数,那我们可以思考一下,最后这两个字符串会被删成什么样子? + +删除的结果不就是它俩的最长公共子序列嘛! + +那么,要计算删除的次数,就可以通过最长公共子序列的长度推导出来: + +```java +int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 复用前文计算 lcs 长度的函数 + int lcs = longestCommonSubsequence(s1, s2); + return m - lcs + n - lcs; +} +``` + +这道题就解决了! + +## 最小 ASCII 删除和 + +这是力扣第 712 题「两个字符串的最小 ASCII 删除和」,题目和上一道题目类似,只不过上道题要求删除次数最小化,这道题要求删掉的字符 ASCII 码之和最小化。 + +函数签名如下: + +```java +int minimumDeleteSum(String s1, String s2) +``` + +比如输入 `s1 = "sea", s2 = "eat"`,算法返回 231。 + +因为在 `"sea"` 中删除 `"s"`,在 `"eat"` 中删除 `"t"`,可使得两个字符串相等,且删掉字符的 ASCII 码之和最小,即 `s(115) + t(116) = 231`。 + +**这道题不能直接复用计算最长公共子序列的函数,但是可以依照之前的思路,稍微修改 base case 和状态转移部分即可直接写出解法代码**: + +```java +class Solution { + // 备忘录 + int memo[][]; + // 主函数 + public int minimumDeleteSum(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 备忘录值为 -1 代表未曾计算 + memo = new int[m][n]; + for (int[] row : memo) + Arrays.fill(row, -1); + + return dp(s1, 0, s2, 0); + } + + // 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串, + // 最小的 ASCII 码之和为 dp(s1, i, s2, j)。 + int dp(String s1, int i, String s2, int j) { + int res = 0; + // base case + if (i == s1.length()) { + // 如果 s1 到头了,那么 s2 剩下的都得删除 + for (; j < s2.length(); j++) + res += s2.charAt(j); + return res; + } + if (j == s2.length()) { + // 如果 s2 到头了,那么 s1 剩下的都得删除 + for (; i < s1.length(); i++) + res += s1.charAt(i); + return res; + } + + if (memo[i][j] != -1) { + return memo[i][j]; + } + + if (s1.charAt(i) == s2.charAt(j)) { + // s1[i] 和 s2[j] 都是在 lcs 中的,不用删除 + memo[i][j] = dp(s1, i + 1, s2, j + 1); + } else { + // s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个 + memo[i][j] = Math.min( + s1.charAt(i) + dp(s1, i + 1, s2, j), + s2.charAt(j) + dp(s1, i, s2, j + 1) + ); + } + return memo[i][j]; + } +} +``` + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +base case 有一定区别,计算 `lcs` 长度时,如果一个字符串为空,那么 `lcs` 长度必然是 0;但是这道题如果一个字符串为空,另一个字符串必然要被全部删除,所以需要计算另一个字符串所有字符的 ASCII 码之和。 + +关于状态转移,当 `s1[i]` 和 `s2[j]` 相同时不需要删除,不同时需要删除,所以可以利用 `dp` 函数计算两种情况,得出最优的结果。其他的大同小异,就不具体展开了。 + +至此,三道子序列问题就解决完了,关键在于将问题细化到字符,根据每两个字符是否相同来判断他们是否在结果子序列中,从而避免了对所有子序列进行穷举。 + +这也算是在两个字符串中求子序列的常用思路吧,建议好好体会,多多练习~ + + + + + + + +
+
+引用本文的文章 + + - [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [97. Interleaving String](https://leetcode.com/problems/interleaving-string/?show=1) | [97. 交错字符串](https://leetcode.cn/problems/interleaving-string/?show=1) | 🟠 | +| - | [剑指 Offer II 095. 最长公共子序列](https://leetcode.cn/problems/qJnOS7/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" index 5075ab2b62..8dae5afb4b 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" @@ -8,6 +8,6 @@ 这就是思维模式的框架,**本章都会按照以上的模式来解决问题,辅助读者养成这种模式思维**,有了方向遇到问题就不会抓瞎,足以解决一般的动态规划问题。 -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" index 7276292f5e..a6991d3049 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" @@ -1,5 +1,32 @@ # 动态规划之KMP字符匹配算法 +

+GitHub + + + +

+ +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [28. Find the Index of the First Occurrence in a String](https://leetcode.com/problems/find-the-index-of-the-first-occurrence-in-a-string/) | [28. 找出字符串中第一个匹配项的下标](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) | 🟠 + +**-----------** + +::: tip + +阅读本文之前,建议你先学习一下另一种字符串匹配算法:[Rabin Karp 字符匹配算法](https://labuladong.online/algo/practice-in-action/rabinkarp/)。 + +::: + KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂。 很多读者抱怨 KMP 算法无法理解,这很正常,想到大学教材上关于 KMP 算法的讲解,也不知道有多少未来的 Knuth、Morris、Pratt 被提前劝退了。有一些优秀的同学通过手推 KMP 算法的过程来辅助理解该算法,这是一种办法,不过本文要从逻辑层面帮助读者理解算法的原理。十行代码之间,KMP 灰飞烟灭。 @@ -10,14 +37,19 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法 读者见过的 KMP 算法应该是,一波诡异的操作处理 `pat` 后形成一个一维的数组 `next`,然后根据这个数组经过又一波复杂操作去匹配 `txt`。时间复杂度 O(N),空间复杂度 O(M)。其实它这个 `next` 数组就相当于 `dp` 数组,其中元素的含义跟 `pat` 的前缀和后缀有关,判定规则比较复杂,不好理解。**本文则用一个二维的 `dp` 数组(但空间复杂度还是 O(M)),重新定义其中元素的含义,使得代码长度大大减少,可解释性大大提高**。 -PS:本文的代码参考《算法4》,原代码使用的数组名称是 `dfa`(确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 `dp` 数组的名称。 +::: note + +本文的代码参考《算法4》,原代码使用的数组名称是 `dfa`(确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 `dp` 数组的名称。 + +::: ### 一、KMP 算法概述 首先还是简单介绍一下 KMP 算法和暴力匹配算法的不同在哪里,难点在哪里,和动态规划有啥关系。 -暴力的字符串匹配算法很容易写,看一下它的运行逻辑: +力扣第 28 题「实现 strStr」就是字符串匹配问题,暴力的字符串匹配算法很容易写,看一下它的运行逻辑: + ```java // 暴力匹配(伪码) int search(String pat, String txt) { @@ -37,21 +69,21 @@ int search(String pat, String txt) { } ``` -对于暴力算法,如果出现不匹配字符,同时回退 `txt` 和 `pat` 的指针,嵌套 for 循环,时间复杂度 $O(MN)$,空间复杂度$O(1)$。最主要的问题是,如果字符串中重复的字符比较多,该算法就显得很蠢。 +对于暴力算法,如果出现不匹配字符,同时回退 `txt` 和 `pat` 的指针,嵌套 for 循环,时间复杂度 `O(MN)`,空间复杂度`O(1)`。最主要的问题是,如果字符串中重复的字符比较多,该算法就显得很蠢。 -比如 txt = "aaacaaab" pat = "aaab": +比如 `txt = "aaacaaab", pat = "aaab"`: -![brutal](../pictures/kmp/1.gif) +![](https://labuladong.online/algo/images/kmp/1.gif) 很明显,`pat` 中根本没有字符 c,根本没必要回退指针 `i`,暴力解法明显多做了很多不必要的操作。 KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明: -![kmp1](../pictures/kmp/2.gif) +![](https://labuladong.online/algo/images/kmp/2.gif) -再比如类似的 txt = "aaaaaaab" pat = "aaab",暴力解法还会和上面那个例子一样蠢蠢地回退指针 `i`,而 KMP 算法又会耍聪明: +再比如类似的 `txt = "aaaaaaab", pat = "aaab"`,暴力解法还会和上面那个例子一样蠢蠢地回退指针 `i`,而 KMP 算法又会耍聪明: -![kmp2](../pictures/kmp/3.gif) +![](https://labuladong.online/algo/images/kmp/3.gif) 因为 KMP 算法知道字符 b 之前的字符 a 都是匹配的,所以每次只需要比较字符 b 是否被匹配就行了。 @@ -74,24 +106,29 @@ pat = "aaab" 只不过对于 `txt1` 的下面这个即将出现的未匹配情况: -![](../pictures/kmp/txt1.jpg) +![](https://labuladong.online/algo/images/kmp/txt1.jpg) `dp` 数组指示 `pat` 这样移动: -![](../pictures/kmp/txt2.jpg) +![](https://labuladong.online/algo/images/kmp/txt2.jpg) + +::: note + +这个`j` 不要理解为索引,它的含义更准确地说应该是**状态**(state),所以它会出现这个奇怪的位置,后文会详述。 -PS:这个`j` 不要理解为索引,它的含义更准确地说应该是**状态**(state),所以它会出现这个奇怪的位置,后文会详述。 +::: 而对于 `txt2` 的下面这个即将出现的未匹配情况: -![](../pictures/kmp/txt3.jpg) +![](https://labuladong.online/algo/images/kmp/txt3.jpg) `dp` 数组指示 `pat` 这样移动: -![](../pictures/kmp/txt4.jpg) +![](https://labuladong.online/algo/images/kmp/txt4.jpg) 明白了 `dp` 数组只和 `pat` 有关,那么我们这样设计 KMP 算法就会比较漂亮: + ```java public class KMP { private int[][] dp; @@ -122,46 +159,45 @@ int pos2 = kmp.search("aaaaaaab"); //4 为什么说 KMP 算法和状态机有关呢?是这样的,我们可以认为 `pat` 的匹配就是状态的转移。比如当 pat = "ABABC": -![](../pictures/kmp/state.jpg) +![](https://labuladong.online/algo/images/kmp/state.jpg) 如上图,圆圈内的数字就是状态,状态 0 是起始状态,状态 5(`pat.length`)是终止状态。开始匹配时 `pat` 处于起始状态,一旦转移到终止状态,就说明在 `txt` 中找到了 `pat`。比如说当前处于状态 2,就说明字符 "AB" 被匹配: -![](../pictures/kmp/state2.jpg) +![](https://labuladong.online/algo/images/kmp/state2.jpg) 另外,处于不同状态时,`pat` 状态转移的行为也不同。比如说假设现在匹配到了状态 4,如果遇到字符 A 就应该转移到状态 3,遇到字符 C 就应该转移到状态 5,如果遇到字符 B 就应该转移到状态 0: -![](../pictures/kmp/state4.jpg) +![](https://labuladong.online/algo/images/kmp/state4.jpg) 具体什么意思呢,我们来一个个举例看看。用变量 `j` 表示指向当前状态的指针,当前 `pat` 匹配到了状态 4: -![](../pictures/kmp/exp1.jpg) +![](https://labuladong.online/algo/images/kmp/exp1.jpg) 如果遇到了字符 "A",根据箭头指示,转移到状态 3 是最聪明的: -![](../pictures/kmp/exp3.jpg) +![](https://labuladong.online/algo/images/kmp/exp3.jpg) 如果遇到了字符 "B",根据箭头指示,只能转移到状态 0(一夜回到解放前): -![](../pictures/kmp/exp5.jpg) +![](https://labuladong.online/algo/images/kmp/exp5.jpg) 如果遇到了字符 "C",根据箭头指示,应该转移到终止状态 5,这也就意味着匹配完成: -![](../pictures/kmp/exp7.jpg) - +![](https://labuladong.online/algo/images/kmp/exp7.jpg) 当然了,还可能遇到其他字符,比如 Z,但是显然应该转移到起始状态 0,因为 `pat` 中根本都没有字符 Z: -![](../pictures/kmp/z.jpg) +![](https://labuladong.online/algo/images/kmp/z.jpg) 这里为了清晰起见,我们画状态图时就把其他字符转移到状态 0 的箭头省略,只画 `pat` 中出现的字符的状态转移: -![](../pictures/kmp/allstate.jpg) +![](https://labuladong.online/algo/images/kmp/allstate.jpg) KMP 算法最关键的步骤就是构造这个状态转移图。**要确定状态转移的行为,得明确两个变量,一个是当前的匹配状态,另一个是遇到的字符**;确定了这两个变量后,就可以知道这个情况下应该转移到哪个状态。 下面看一下 KMP 算法根据这幅状态转移图匹配字符串 `txt` 的过程: -![](../pictures/kmp/kmp.gif) +![](https://labuladong.online/algo/images/kmp/kmp.gif) **请记住这个 GIF 的匹配过程,这就是 KMP 算法的核心逻辑**! @@ -184,6 +220,7 @@ pat 应该转移到状态 2 根据我们这个 dp 数组的定义和刚才状态转移的过程,我们可以先写出 KMP 算法的 search 函数代码: + ```java public int search(String txt) { int M = pat.length(); @@ -216,29 +253,29 @@ for 0 <= j < M: # 状态 这个 next 状态应该怎么求呢?显然,**如果遇到的字符 `c` 和 `pat[j]` 匹配的话**,状态就应该向前推进一个,也就是说 `next = j + 1`,我们不妨称这种情况为**状态推进**: -![](../pictures/kmp/forward.jpg) +![](https://labuladong.online/algo/images/kmp/forward.jpg) **如果字符 `c` 和 `pat[j]` 不匹配的话**,状态就要回退(或者原地不动),我们不妨称这种情况为**状态重启**: -![](../pictures/kmp/back.jpg) +![](https://labuladong.online/algo/images/kmp/back.jpg) 那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义一个名字:**影子状态**(我编的名字),用变量 `X` 表示。**所谓影子状态,就是和当前状态具有相同的前缀**。比如下面这种情况: -![](../pictures/kmp/shadow.jpg) +![](https://labuladong.online/algo/images/kmp/shadow.jpg) 当前状态 `j = 4`,其影子状态为 `X = 2`,它们都有相同的前缀 "AB"。因为状态 `X` 和状态 `j` 存在相同的前缀,所以当状态 `j` 准备进行状态重启的时候(遇到的字符 `c` 和 `pat[j]` 不匹配),可以通过 `X` 的状态转移图来获得**最近的重启位置**。 比如说刚才的情况,如果状态 `j` 遇到一个字符 "A",应该转移到哪里呢?首先只有遇到 "C" 才能推进状态,遇到 "A" 显然只能进行状态重启。**状态 `j` 会把这个字符委托给状态 `X` 处理,也就是 `dp[j]['A'] = dp[X]['A']`**: -![](../pictures/kmp/shadow1.jpg) +![](https://labuladong.online/algo/images/kmp/shadow1.jpg) 为什么这样可以呢?因为:既然 `j` 这边已经确定字符 "A" 无法推进状态,**只能回退**,而且 KMP 就是要**尽可能少的回退**,以免多余的计算。那么 `j` 就可以去问问和自己具有相同前缀的 `X`,如果 `X` 遇见 "A" 可以进行「状态推进」,那就转移过去,因为这样回退最少。 -![](../pictures/kmp/A.gif) +![](https://labuladong.online/algo/images/kmp/A.gif) 当然,如果遇到的字符是 "B",状态 `X` 也不能进行「状态推进」,只能回退,`j` 只要跟着 `X` 指引的方向回退就行了: -![](../pictures/kmp/shadow2.jpg) +![](https://labuladong.online/algo/images/kmp/shadow2.jpg) 你也许会问,这个 `X` 怎么知道遇到字符 "B" 要回退到状态 0 呢?因为 `X` 永远跟在 `j` 的身后,状态 `X` 如何转移,在之前就已经算出来了。动态规划算法不就是利用过去的结果解决现在的问题吗? @@ -261,6 +298,7 @@ for 0 <= j < M: 如果之前的内容你都能理解,恭喜你,现在就剩下一个问题:影子状态 `X` 是如何得到的呢?下面先直接看完整代码吧。 + ```java public class KMP { private int[][] dp; @@ -332,10 +370,11 @@ for (int i = 0; i < N; i++) { 下面来看一下状态转移图的完整构造过程,你就能理解状态 `X` 作用之精妙了: -![](../pictures/kmp/dfa.gif) +![](https://labuladong.online/algo/images/kmp/dfa.gif) 至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧: + ```java public class KMP { private int[][] dp; @@ -399,12 +438,149 @@ public class KMP { KMP 算法也就是动态规划那点事,我们的公众号文章目录有一系列专门讲动态规划的,而且都是按照一套框架来的,无非就是描述问题逻辑,明确 `dp` 数组含义,定义 base case 这点破事。希望这篇文章能让大家对动态规划有更深的理解。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:贪心算法之区间调度问题](../动态规划系列/贪心算法之区间调度问题.md) +
+
+引用本文的文章 + + - [我的刷题心得:算法的本质](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [滑动窗口算法延伸:Rabin Karp 字符匹配算法](https://labuladong.online/algo/practice-in-action/rabinkarp/) + +

+ + + + + +**_____________** + +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: + +![](https://labuladong.online/algo/images/souyisou2.png) + +======其他语言代码====== + +[28.实现 strStr()](https://leetcode-cn.com/problems/implement-strstr) + +### python + +[MoguCloud](https://github.com/MoguCloud) 提供 实现 strStr() 的 Python 完整代码: + +```python +class Solution: + def strStr(self, haystack: str, needle: str) -> int: + # 边界条件判断 + if not needle: + return 0 + pat = needle + txt = haystack + + M = len(pat) + # dp[状态][字符] = 下个状态 + dp = [[0 for _ in range(256)] for _ in pat] + # base case + dp[0][ord(pat[0])] = 1 + # 影子状态 X 初始化为 0 + X = 0 + for j in range(1, M): + for c in range(256): + dp[j][c] = dp[X][c] + dp[j][ord(pat[j])] = j + 1 + # 更新影子状态 + X = dp[X][ord(pat[j])] + + N = len(txt) + # pat 初始状态为 0 + j = 0 + for i in range(N): + # 计算 pat 的下一个状态 + j = dp[j][ord(txt[i])] + # 到达终止态,返回结果 + if j == M: + return i - M + 1 + # 没到达终止态,匹配失败 + return -1 +``` + + + +### javascript + +```js +class KMP { + constructor(pat) { + this.pat = pat; + let m = pat.length; + + // dp[状态][字符] = 下个状态 初始化一个m*256的整数矩阵 + this.dp = new Array(m); + for (let i = 0; i < m; i++) { + this.dp[i] = new Array(256); + this.dp[i].fill(0, 0, 256); + } + + // base case + this.dp[0][this.pat[0].charCodeAt()] = 1; + + // 影子状态X 初始为0 + let x = 0; + + // 构建状态转移图 + for (let j = 1; j < m; j++) { + for (let c = 0; c < 256; c++) { + this.dp[j][c] = this.dp[x][c]; + } + + // dp[][对应的ASCII码] + this.dp[j][this.pat[j].charCodeAt()] = j + 1; + + // 更新影子状态 + x = this.dp[x][this.pat[j].charCodeAt()] + } + } + + search(txt) { + + let m = this.pat.length; + let n = txt.length; + + // pat的初始态为0 + let j = 0; + for (let i = 0; i < n; i++) { + // 计算pat的下一个状态 + j = this.dp[j][txt[i].charCodeAt()]; + + // 到达终止态 返回结果 + if (j === m) return i - m + 1; + } + + // 没到终止态 匹配失败 + return -1; + } + +} + +/** + * @param {string} haystack + * @param {string} needle + * @return {number} + */ +var strStr = function(haystack, needle) { + if(haystack === ""){ + if(needle !== ""){ + return -1; + } + return 0; + } + + if(needle === ""){ + return 0; + } + let kmp = new KMP(needle); + return kmp.search(haystack) +}; +``` + -[下一篇:团灭 LeetCode 股票买卖问题](../动态规划系列/团灭股票问题.md) -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" index 9c869039ec..d3c64f5d6a 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" @@ -1,32 +1,89 @@ -# 动态规划之博弈问题 +# 经典动态规划:博弈问题 -上一篇文章 [几道智力题](../高频面试系列/一行代码解决的智力题.md) 中讨论到一个有趣的「石头游戏」,通过题目的限制条件,这个游戏是先手必胜的。但是智力题终究是智力题,真正的算法问题肯定不会是投机取巧能搞定的。所以,本文就借石头游戏来讲讲「假设两个人都足够聪明,最后谁会获胜」这一类问题该如何用动态规划算法解决。 -博弈类问题的套路都差不多,下文举例讲解,其核心思路是在二维 dp 的基础上使用元组分别存储两个人的博弈结果。掌握了这个技巧以后,别人再问你什么俩海盗分宝石,俩人拿硬币的问题,你就告诉别人:我懒得想,直接给你写个算法算一下得了。 -我们「石头游戏」改的更具有一般性: +![](https://labuladong.online/algo/images/souyisou1.png) -你和你的朋友面前有一排石头堆,用一个数组 piles 表示,piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。 +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [486. Predict the Winner](https://leetcode.com/problems/predict-the-winner/) | [486. 预测赢家](https://leetcode.cn/problems/predict-the-winner/) | 🟠 | +| [877. Stone Game](https://leetcode.com/problems/stone-game/) | [877. 石子游戏](https://leetcode.cn/problems/stone-game/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +上一篇文章 [几道智力题](https://labuladong.online/algo/frequency-interview/one-line-solutions/) 中讨论到一个有趣的「石头游戏」,通过题目的限制条件,这个游戏是先手必胜的。但是智力题终究是智力题,真正的算法问题肯定不会是投机取巧能搞定的。所以,本文就借石头游戏来讲讲「假设两个人都足够聪明,最后谁会获胜」这一类问题该如何用动态规划算法解决。 + +博弈类问题的套路都差不多,下文参考 [这个 YouTube 视频](https://www.youtube.com/watch?v=WxpIHvsu1RI) 的思路讲解,其核心思路是在二维 dp 的基础上使用元组分别存储两个人的博弈结果。掌握了这个技巧以后,别人再问你什么俩海盗分宝石,俩人拿硬币的问题,你就告诉别人:我懒得想,直接给你写个算法算一下得了。 + + + + + + + +我们把力扣第 877 题「石头游戏」改的更具有一般性: + +你和你的朋友面前有一排石头堆,用一个数组 `piles` 表示,`piles[i]` 表示第 `i` 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。 石头的堆数可以是任意正整数,石头的总数也可以是任意正整数,这样就能打破先手必胜的局面了。比如有三堆石头 `piles = [1, 100, 3]`,先手不管拿 1 还是 3,能够决定胜负的 100 都会被后手拿走,后手会获胜。 -**假设两人都很聪明**,请你设计一个算法,返回先手和后手的最后得分(石头总数)之差。比如上面那个例子,先手能获得 4 分,后手会获得 100 分,你的算法应该返回 -96。 +**假设两人都很聪明**,请你写一个 `stoneGame` 函数,返回先手和后手的最后得分(石头总数)之差。比如上面那个例子,先手能获得 4 分,后手会获得 100 分,你的算法应该返回 -96: + +```java +int stoneGame(int[] nums); +``` + +这样推广之后就变成了一道难度比较高的动态规划问题了,力扣第 486 题「预测赢家」就是一道类似的问题: + + + +函数签名如下: + +```java +boolean predictTheWinner(int[] nums); +``` + +那么如果有了一个计算先手和后手分差的 `stoneGame` 函数,这道题的解法就直接出来了: + +```java +public boolean predictTheWinner(int[] nums) { + // 先手的分数大于等于后手,则能赢 + return stoneGame(nums) >= 0; +} +``` + +这个 `stoneGame` 函数怎么写呢?博弈问题的难点在于,两个人要轮流进行选择,而且都贼精明,应该如何编程表示这个过程呢?其实不难,还是按照 [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 中强调多次的套路,首先明确 `dp` 数组的含义,然后只要找到「状态」和「选择」,一切就水到渠成了。 + +## 一、定义 `dp` 数组的含义 + +定义 `dp` 数组的含义是很有技术含量的,同一问题可能有多种定义方法,不同的定义会引出不同的状态转移方程,不过只要逻辑没有问题,最终都能得到相同的答案。 -这样推广之后,这个问题算是一道 Hard 的动态规划问题了。**博弈问题的难点在于,两个人要轮流进行选择,而且都贼精明,应该如何编程表示这个过程呢?** +我建议不要迷恋那些看起来很牛逼,代码很短小的解法思路,最好是稳一点,采取可解释性最好,最容易推广的解法思路。本文就给出一种博弈问题的通用设计框架。 -还是强调多次的套路,首先明确 dp 数组的含义,然后和股票买卖系列问题类似,只要找到「状态」和「选择」,一切就水到渠成了。 +介绍 `dp` 数组的含义之前,我们先看一下 `dp` 数组最终的样子: + +![](https://labuladong.online/algo/images/stone-game/1.png) + +下文讲解时,认为元组是包含 `first` 和 `second` 属性的一个类,而且为了节省篇幅,将这两个属性简写为 `fir` 和 `sec`。比如按上图的数据,我们说 `dp[1][3].fir = 11`,`dp[0][1].sec = 2`。 -### 一、定义 dp 数组的含义 -定义 dp 数组的含义是很有技术含量的,同一问题可能有多种定义方法,不同的定义会引出不同的状态转移方程,不过只要逻辑没有问题,最终都能得到相同的答案。 -我建议不要迷恋那些看起来很牛逼,代码很短小的奇技淫巧,最好是稳一点,采取可解释性最好,最容易推广的设计思路。本文就给出一种博弈问题的通用设计框架。 -介绍 dp 数组的含义之前,我们先看一下 dp 数组最终的样子: -![1](../pictures/博弈问题/1.png) -下文讲解时,认为元组是包含 first 和 second 属性的一个类,而且为了节省篇幅,将这两个属性简写为 fir 和 sec。比如按上图的数据,我们说 `dp[1][3].fir = 10`,`dp[0][1].sec = 3`。 先回答几个读者可能提出的问题: @@ -34,22 +91,27 @@ **以下是对 dp 数组含义的解释:** -```python -dp[i][j].fir 表示,对于 piles[i...j] 这部分石头堆,先手能获得的最高分数。 -dp[i][j].sec 表示,对于 piles[i...j] 这部分石头堆,后手能获得的最高分数。 +`dp[i][j].fir = x` 表示,对于 `piles[i...j]` 这部分石头堆,先手能获得的最高分数为 `x`。 -举例理解一下,假设 piles = [3, 9, 1, 2],索引从 0 开始 -dp[0][1].fir = 9 意味着:面对石头堆 [3, 9],先手最终能够获得 9 分。 -dp[1][3].sec = 2 意味着:面对石头堆 [9, 1, 2],后手最终能够获得 2 分。 -``` +`dp[i][j].sec = y` 表示,对于 `piles[i...j]` 这部分石头堆,后手能获得的最高分数为 `y`。 + +举例理解一下,假设 `piles = [2, 8, 3, 5]`,索引从 0 开始,那么: -我们想求的答案是先手和后手最终分数之差,按照这个定义也就是 $dp[0][n-1].fir - dp[0][n-1].sec$,即面对整个 piles,先手的最优得分和后手的最优得分之差。 +`dp[0][1].fir = 8` 意味着:面对石头堆 `[2, 8]`,先手最多能够获得 8 分;`dp[1][3].sec = 5` 意味着:面对石头堆 `[8, 3, 5]`,后手最多能够获得 5 分。 -### 二、状态转移方程 +我们想求的答案是先手和后手最终分数之差,按照这个定义也就是 `dp[0][n-1].fir - dp[0][n-1].sec`,即面对整个 `piles`,先手的最优得分和后手的最优得分之差。 + + + + + + + +## 二、状态转移方程 写状态转移方程很简单,首先要找到所有「状态」和每个状态可以做的「选择」,然后择优。 -根据前面对 dp 数组的定义,**状态显然有三个:开始的索引 i,结束的索引 j,当前轮到的人。** +根据前面对 `dp` 数组的定义,**状态显然有三个:开始的索引 `i`,结束的索引 `j`,当前轮到的人。** ```python dp[i][j][fir or sec] @@ -66,35 +128,48 @@ for 0 <= i < n: for j <= i < n: for who in {fir, sec}: dp[i][j][who] = max(left, right) - ``` -上面的伪码是动态规划的一个大致的框架,股票系列问题中也有类似的伪码。这道题的难点在于,两人是交替进行选择的,也就是说先手的选择会对后手有影响,这怎么表达出来呢? +上面的伪码是动态规划的一个大致的框架,这道题的难点在于,两人足够聪明,而且是交替进行选择的,也就是说先手的选择会对后手有影响,这怎么表达出来呢? + +根据我们对 `dp` 数组的定义,很容易解决这个难点,**写出状态转移方程**: + + + -根据我们对 dp 数组的定义,很容易解决这个难点,**写出状态转移方程:** ```python dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec) -dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 ) +dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 ) # 解释:我作为先手,面对 piles[i...j] 时,有两种选择: -# 要么我选择最左边的那一堆石头,然后面对 piles[i+1...j] -# 但是此时轮到对方,相当于我变成了后手; -# 要么我选择最右边的那一堆石头,然后面对 piles[i...j-1] -# 但是此时轮到对方,相当于我变成了后手。 + +# 要么我选择最左边的那一堆石头 piles[i],局面变成了 piles[i+1...j], +# 然后轮到对方选了,我变成了后手,此时我作为后手的最优得分是 dp[i+1][j].sec + +# 要么我选择最右边的那一堆石头 piles[j],局面变成了 piles[i...j-1] +# 然后轮到对方选了,我变成了后手,此时我作为后手的最优得分是 dp[i][j-1].sec if 先手选择左边: dp[i][j].sec = dp[i+1][j].fir if 先手选择右边: dp[i][j].sec = dp[i][j-1].fir # 解释:我作为后手,要等先手先选择,有两种情况: + # 如果先手选择了最左边那堆,给我剩下了 piles[i+1...j] -# 此时轮到我,我变成了先手; +# 此时轮到我,我变成了先手,此时的最优得分是 dp[i+1][j].fir + # 如果先手选择了最右边那堆,给我剩下了 piles[i...j-1] -# 此时轮到我,我变成了先手。 +# 此时轮到我,我变成了先手,此时的最优得分是 dp[i][j-1].fir ``` + + 根据 dp 数组的定义,我们也可以找出 **base case**,也就是最简单的情况: + + + + ```python dp[i][j].fir = piles[i] dp[i][j].sec = 0 @@ -104,20 +179,27 @@ dp[i][j].sec = 0 # 后手没有石头拿了,得分为 0 ``` -![2](../pictures/博弈问题/2.png) +![](https://labuladong.online/algo/images/stone-game/2.png) + -这里需要注意一点,我们发现 base case 是斜着的,而且我们推算 dp[i][j] 时需要用到 dp[i+1][j] 和 dp[i][j-1]: -![3](../pictures/博弈问题/3.png) +这里需要注意一点,我们发现 base case 是斜着的,而且我们推算 `dp[i][j]` 时需要用到 `dp[i+1][j]` 和 `dp[i][j-1]`: -所以说算法不能简单的一行一行遍历 dp 数组,**而要斜着遍历数组:** +![](https://labuladong.online/algo/images/stone-game/3.png) -![4](../pictures/博弈问题/4.png) +根据前文 [动态规划答疑篇](https://labuladong.online/algo/dynamic-programming/faq-summary/) 判断 `dp` 数组遍历方向的原则,算法应该倒着遍历 `dp` 数组: -说实话,斜着遍历二维数组说起来容易,你还真不一定能想出来怎么实现,不信你思考一下?这么巧妙的状态转移方程都列出来了,要是不会写代码实现,那真的很尴尬了。 +```java +for (int i = n - 2; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + dp[i][j] = ... + } +} +``` +![](https://labuladong.online/algo/images/stone-game/4.png) -### 三、代码实现 +## 三、代码实现 如何实现这个 fir 和 sec 元组呢,你可以用 python,自带元组类型;或者使用 C++ 的 pair 容器;或者用一个三维数组 `dp[n][n][2]`,最后一个维度就相当于元组;或者我们自己写一个 Pair 类: @@ -131,10 +213,10 @@ class Pair { } ``` -然后直接把我们的状态转移方程翻译成代码即可,可以注意一下斜着遍历数组的技巧: +然后直接把我们的状态转移方程翻译成代码即可,注意我们要倒着遍历数组: ```java -/* 返回游戏最后先手和后手的得分之差 */ +// 返回游戏最后先手和后手的得分之差 int stoneGame(int[] piles) { int n = piles.length; // 初始化 dp 数组 @@ -147,14 +229,15 @@ int stoneGame(int[] piles) { dp[i][i].fir = piles[i]; dp[i][i].sec = 0; } - // 斜着遍历数组 - for (int l = 2; l <= n; l++) { - for (int i = 0; i <= n - l; i++) { - int j = l + i - 1; + + // 倒着遍历数组 + for (int i = n - 2; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { // 先手选择最左边或最右边的分数 int left = piles[i] + dp[i+1][j].sec; int right = piles[j] + dp[i][j-1].sec; // 套用状态转移方程 + // 先手肯定会选择更大的结果,后手的选择随之改变 if (left > right) { dp[i][j].fir = left; dp[i][j].sec = dp[i+1][j].fir; @@ -171,24 +254,36 @@ int stoneGame(int[] piles) { 动态规划解法,如果没有状态转移方程指导,绝对是一头雾水,但是根据前面的详细解释,读者应该可以清晰理解这一大段代码的含义。 -而且,注意到计算 `dp[i][j]` 只依赖其左边和下边的元素,所以说肯定有优化空间,转换成一维 dp,想象一下把二维平面压扁,也就是投影到一维。但是,一维 dp 比较复杂,可解释性很差,大家就不必浪费这个时间去理解了。 +而且,注意到计算 `dp[i][j]` 只依赖其左边和下边的元素,所以说肯定有优化空间,转换成一维 dp,想象一下把二维平面压扁,也就是投影到一维。但是,一维 `dp` 比较复杂,可解释性比较差,大家就不必浪费这个时间去理解了。 -### 四、最后总结 +## 四、最后总结 本文给出了解决博弈问题的动态规划解法。博弈问题的前提一般都是在两个聪明人之间进行,编程描述这种游戏的一般方法是二维 dp 数组,数组中通过元组分别表示两人的最优决策。 -之所以这样设计,是因为先手在做出选择之后,就成了后手,后手在对方做完选择后,就变成了先手。这种角色转换使得我们可以重用之前的结果,典型的动态规划标志。 +之所以这样设计,是因为先手在做出选择之后,就成了后手,后手在对方做完选择后,就变成了先手。**这种角色转换使得我们可以重用之前的结果,典型的动态规划标志**。 + +读到这里的朋友应该能理解算法解决博弈问题的套路了。学习算法,一定要注重算法的模板框架,而不是一些看起来牛逼的思路,也不要奢求上来就写一个最优的解法。不要舍不得多用空间,不要过早尝试优化,不要惧怕多维数组。`dp` 数组就是存储信息避免重复计算的,随便用,直到咱满意为止。 + + + + + + + +
+
+引用本文的文章 + + - [贪心算法之区间调度问题](https://labuladong.online/algo/frequency-interview/interval-scheduling/) + +

+ -读到这里的朋友应该能理解算法解决博弈问题的套路了。学习算法,一定要注重算法的模板框架,而不是一些看起来牛逼的思路,也不要奢求上来就写一个最优的解法。不要舍不得多用空间,不要过早尝试优化,不要惧怕多维数组。dp 数组就是存储信息避免重复计算的,随便用,直到咱满意为止。 -希望本文对你有帮助。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:动态规划之子序列问题解题模板](../动态规划系列/子序列问题模板.md) -[下一篇:贪心算法之区间调度问题](../动态规划系列/贪心算法之区间调度问题.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" index 2605d4c9f2..b6a5a79cf6 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" @@ -1,12 +1,58 @@ # 动态规划之四键键盘 -四键键盘问题很有意思,而且可以明显感受到:对 dp 数组的不同定义需要完全不同的逻辑,从而产生完全不同的解法。 +

+GitHub + + + +

+ +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [651. 4 Keys Keyboard](https://leetcode.com/problems/4-keys-keyboard/)🔒 | [651. 4键键盘](https://leetcode.cn/problems/4-keys-keyboard/)🔒 | 🟠 + +**-----------** + +力扣第 651 题「四键键盘」很有意思,而且可以明显感受到:对 `dp` 数组的不同定义需要完全不同的逻辑,从而产生完全不同的解法。 首先看一下题目: -![](../pictures/4keyboard/title.png) +假设你有一个特殊的键盘,上面只有四个键,它们分别是: -如何在 N 次敲击按钮后得到最多的 A?我们穷举呗,每次有对于每次按键,我们可以穷举四种可能,很明显就是一个动态规划问题。 +1、`A` 键:在屏幕上打印一个 `A`。 + +2、`Ctrl-A` 键:选中整个屏幕。 + +3、`Ctrl-C` 键:复制选中的区域到缓冲区。 + +4、`Ctrl-V` 键:将缓冲区的内容输入到光标所在的屏幕上。 + +这就和我们平时使用的全选复制粘贴功能完全相同嘛,只不过题目把 `Ctrl` 的组合键视为了一个键。现在要求你只能进行 `N` 次操作,请你计算屏幕上最多能显示多少个 `A`? + +函数签名如下: + + +```java +int maxA(int N); +``` + +比如说输入 `N = 3`,算法返回 3,因为连按 3 次 `A` 键是最优的方案。 + +如果输入是 `N = 7`,则算法返回 9,最优的操作序列如下: + +`A`, `A`, `A`, `Ctrl-A`, `Ctrl-C`, `Ctrl-V`, `Ctrl-V` + +可以得到 9 个 `A`。 + +如何在 `N` 次敲击按钮后得到最多的 `A`?我们穷举呗,每次有对于每次按键,我们可以穷举四种可能,很明显就是一个动态规划问题。 ### 第一种思路 @@ -39,6 +85,7 @@ dp(n - 2, a_num, a_num) # C-A C-C 这样可以看到问题的规模 `n` 在不断减小,肯定可以到达 `n = 0` 的 base case,所以这个思路是正确的: + ```python def maxA(N: int) -> int: @@ -60,6 +107,7 @@ def maxA(N: int) -> int: 这个解法应该很好理解,因为语义明确。下面就继续走流程,用备忘录消除一下重叠子问题: + ```python def maxA(N: int) -> int: # 备忘录 @@ -87,7 +135,7 @@ dp[n][a_num][copy] # 状态的总数(时空复杂度)就是这个三维数组的体积 ``` -我们知道变量 `n` 最多为 `N`,但是 `a_num` 和 `copy` 最多为多少我们很难计算,复杂度起码也有 O(N^3) 把。所以这个算法并不好,复杂度太高,且已经无法优化了。 +我们知道变量 `n` 最多为 `N`,但是 `a_num` 和 `copy` 最多为多少我们很难计算,复杂度起码也有 O(N^3) 吧。所以这个算法并不好,复杂度太高,且已经无法优化了。 这也就说明,我们这样定义「状态」是不太优秀的,下面我们换一种定义 dp 的思路。 @@ -124,7 +172,8 @@ dp[i] = dp[i - 1] + 1; 但是,如果要按 `C-V`,还要考虑之前是在哪里 `C-A C-C` 的。 **刚才说了,最优的操作序列一定是 `C-A C-C` 接着若干 `C-V`,所以我们用一个变量 `j` 作为若干 `C-V` 的起点**。那么 `j` 之前的 2 个操作就应该是 `C-A C-C` 了: - + + ```java public int maxA(int N) { int[] dp = new int[N + 1]; @@ -145,7 +194,7 @@ public int maxA(int N) { 其中 `j` 变量减 2 是给 `C-A C-C` 留下操作数,看个图就明白了: -![](../pictures/4keyboard/1.jpg) +![](https://labuladong.online/algo/images/4keyboard/1.jpg) 这样,此算法就完成了,时间复杂度 O(N^2),空间复杂度 O(N),这种解法应该是比较高效的了。 @@ -168,13 +217,81 @@ def dp(n, a_num, copy): 根据这个事实,我们重新定义了状态,重新寻找了状态转移,从逻辑上减少了无效的子问题个数,从而提高了算法的效率。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +
+
+引用本文的文章 + + - [一个方法团灭 LeetCode 打家劫舍问题](https://labuladong.online/algo/dynamic-programming/house-robber/) + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + +

+ + + + + +**_____________** -[上一篇:团灭 LeetCode 打家劫舍问题](../动态规划系列/抢房子.md) +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: -[下一篇:动态规划之正则表达](../动态规划系列/动态规划之正则表达.md) +![](https://labuladong.online/algo/images/souyisou2.png) + +======其他语言代码====== + +### javascript + +[651.四键键盘](https://leetcode-cn.com/problems/4-keys-keyboard) + +**1、第一种思路** + +```js +let maxA = function (N) { + // 备忘录 + let memo = {} + + let dp = function (n, a_num, copy) { + if (n <= 0) { + return a_num; + } + + let key = n + ',' + a_num + ',' + copy + // 避免计算重叠子问题 + if (memo[key] !== undefined) { + return memo[key] + } + + memo[key] = Math.max( + dp(n - 1, a_num + 1, copy), // A + dp(n - 1, a_num + copy, copy), // C-V + dp(n - 2, a_num, a_num) // C-A C-C + ) + + return memo[key] + } + + return dp(N, 0, 0) +} +``` + +**2、第二种思路** + +```js +var maxA = function (N) { + let dp = new Array(N + 1); + dp[0] = 0; + for (let i = 1; i <= N; i++) { + // 按A键盘 + dp[i] = dp[i - 1] + 1; + for (let j = 2; j < i; j++) { + // 全选 & 复制 dp[j-2],连续粘贴 i - j 次 + // 屏幕上共 dp[j - 2] * (i - j + 1) 个 A + dp[i] = Math.max(dp[i], dp[j - 2] * (i - (j - 2) - 1)); + } + } + // N 次按键之后最多有几个 A? + return dp[N]; +} +``` -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" index 605bccc2c9..bad9314251 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" @@ -1,177 +1,379 @@ -# 动态规划之正则表达 +# 经典动态规划:正则表达式 -之前的文章「动态规划详解」收到了普遍的好评,今天写一个动态规划的实际应用:正则表达式。如果有读者对「动态规划」还不了解,建议先看一下上面那篇文章。 -正则表达式匹配是一个很精妙的算法,而且难度也不小。本文主要写两个正则符号的算法实现:点号「.」和星号「*」,如果你用过正则表达式,应该明白他们的用法,不明白也没关系,等会会介绍。文章的最后,介绍了一种快速看出重叠子问题的技巧。 -本文还有一个重要目的,就是教会读者如何设计算法。我们平时看别人的解法,直接看到一个面面俱到的完整答案,总觉得无法理解,以至觉得问题太难,自己太菜。我力求向读者展示,算法的设计是一个螺旋上升、逐步求精的过程,绝不是一步到位就能写出正确算法。本文会带你解决这个较为复杂的问题,让你明白如何化繁为简,逐个击破,从最简单的框架搭建出最终的答案。 +![](https://labuladong.online/algo/images/souyisou1.png) -前文无数次强调的框架思维,就是在这种设计过程中逐步培养的。下面进入正题,首先看一下题目: +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -![title](../pictures/%E6%AD%A3%E5%88%99/title.png) -### 一、热身 -第一步,我们暂时不管正则符号,如果是两个普通的字符串进行比较,如何进行匹配?我想这个算法应该谁都会写: +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: -```cpp -bool isMatch(string text, string pattern) { - if (text.size() != pattern.size()) - return false; - for (int j = 0; j < pattern.size(); j++) { - if (pattern[j] != text[j]) +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [10. Regular Expression Matching](https://leetcode.com/problems/regular-expression-matching/) | [10. 正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) +> - [经典动态规划:编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/) + +正则表达式是一个非常强力的工具,本文就来具体看一看正则表达式的底层原理是什么。力扣第 10 题「正则表达式匹配」就要求我们实现一个简单的正则匹配算法,包括「.」通配符和「*」通配符。 + +这两个通配符是最常用的,其中点号「.」可以匹配任意一个字符,星号「*」可以让之前的那个字符重复任意次数(包括 0 次)。 + +比如说模式串 `".a*b"` 就可以匹配文本 `"zaaab"`,也可以匹配 `"cb"`;模式串 `"a..b"` 可以匹配文本 `"amnb"`;而模式串 `".*"` 就比较牛逼了,它可以匹配任何文本。 + +题目会给我们输入两个字符串 `s` 和 `p`,`s` 代表文本,`p` 代表模式串,请你判断模式串 `p` 是否可以匹配文本 `s`。我们可以假设模式串只包含小写字母和上述两种通配符且一定合法,不会出现 `*a` 或者 `b**` 这种不合法的模式串, + +函数签名如下: + +```java +boolean isMatch(string s, string p); +``` + +对于我们将要实现的这个正则表达式,难点在那里呢? + +点号通配符其实很好实现,`s` 中的任何字符,只要遇到 `.` 通配符,无脑匹配就完事了。主要是这个星号通配符不好实现,一旦遇到 `*` 通配符,前面的那个字符可以选择重复一次,可以重复多次,也可以一次都不出现,这该怎么办? + +对于这个问题,答案很简单,对于所有可能出现的情况,全部穷举一遍,只要有一种情况可以完成匹配,就认为 `p` 可以匹配 `s`。那么一旦涉及两个字符串的穷举,我们就应该条件反射地想到动态规划的技巧了。 + +## 一、思路分析 + +我们先脑补一下,`s` 和 `p` 相互匹配的过程大致是,两个指针 `i, j` 分别在 `s` 和 `p` 上移动,如果最后两个指针都能移动到字符串的末尾,那么久匹配成功,反之则匹配失败。 + +**如果不考虑 `*` 通配符,面对两个待匹配字符 `s[i]` 和 `p[j]`,我们唯一能做的就是看他俩是否匹配**: + +```java +boolean isMatch(String s, String p) { + int i = 0, j = 0; + while (i < s.length() && j < p.length()) { + // 「.」通配符就是万金油 + if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { + // 匹配,接着匹配 s[i+1..] 和 p[j+1..] + i++; j++; + } else { + // 不匹配 return false; + } } - return true; + return i == j; } ``` -然后,我稍微改造一下上面的代码,略微复杂了一点,但意思还是一样的,很容易理解吧: +那么考虑一下,如果加入 `*` 通配符,局面就会稍微复杂一些,不过只要分情况来分析,也不难理解。 -```cpp -bool isMatch(string text, string pattern) { - int i = 0; // text 的索引位置 - int j = 0; // pattern 的索引位置 - while (j < pattern.size()) { - if (i >= text.size()) - return false; - if (pattern[j++] != text[i++]) - return false; +**当 `p[j + 1]` 为 `*` 通配符时,我们分情况讨论下**: + +1、如果 `s[i] == p[j]`,那么有两种情况: + +1.1 `p[j]` 有可能会匹配多个字符,比如 `s = "aaa", p = "a*"`,那么 `p[0]` 会通过 `*` 匹配 3 个字符 `"a"`。 + +1.2 `p[i]` 也有可能匹配 0 个字符,比如 `s = "aa", p = "a*aa"`,由于后面的字符可以匹配 `s`,所以 `p[0]` 只能匹配 0 次。 + +2、如果 `s[i] != p[j]`,只有一种情况: + +`p[j]` 只能匹配 0 次,然后看下一个字符是否能和 `s[i]` 匹配。比如说 `s = "aa", p = "b*aa"`,此时 `p[0]` 只能匹配 0 次。 + +综上,可以把之前的代码针对 `*` 通配符进行一下改造: + +```java +if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { + // 匹配 + if (j < p.length() - 1 && p.charAt(j + 1) == '*') { + // 有 * 通配符,可以匹配 0 次或多次 + } else { + // 无 * 通配符,老老实实匹配 1 次 + i++; j++; + } +} else { + // 不匹配 + if (j < p.length() - 1 && p.charAt(j + 1) == '*') { + // 有 * 通配符,只能匹配 0 次 + } else { + // 无 * 通配符,匹配无法进行下去了 + return false; } - // 相等则说明完成匹配 - return j == text.size(); } ``` -如上改写,是为了将这个算法改造成递归算法(伪码): +整体的思路已经很清晰了,但现在的问题是,遇到 `*` 通配符时,到底应该匹配 0 次还是匹配多次?多次是几次? -```python -def isMatch(text, pattern) -> bool: - if pattern is empty: return (text is empty?) - first_match = (text not empty) and pattern[0] == text[0] - return first_match and isMatch(text[1:], pattern[1:]) -``` +你看,这就是一个做「选择」的问题,要把所有可能的选择都穷举一遍才能得出结果。动态规划算法的核心就是「状态」和「选择」,**「状态」无非就是 `i` 和 `j` 两个指针的位置,「选择」就是 `p[j]` 选择匹配几个字符**。 + +## 二、动态规划解法 + +根据「状态」,我们可以定义一个 `dp` 函数: -如果你能够理解这段代码,恭喜你,你的递归思想已经到位,正则表达式算法虽然有点复杂,其实是基于这段递归代码逐步改造而成的。 -### 二、处理点号「.」通配符 -点号可以匹配任意一个字符,万金油嘛,其实是最简单的,稍加改造即可: -```python -def isMatch(text, pattern) -> bool: - if not pattern: return not text - first_match = bool(text) and pattern[0] in {text[0], '.'} - return first_match and isMatch(text[1:], pattern[1:]) + +```java +boolean dp(String s, int i, String p, int j); ``` -### 三、处理「*」通配符 -星号通配符可以让前一个字符重复任意次数,包括零次。那到底是重复几次呢?这似乎有点困难,不过不要着急,我们起码可以把框架的搭建再进一步: -```python -def isMatch(text, pattern) -> bool: - if not pattern: return not text - first_match = bool(text) and pattern[0] in {text[0], '.'} - if len(pattern) >= 2 and pattern[1] == '*': - # 发现 '*' 通配符 - else: - return first_match and isMatch(text[1:], pattern[1:]) +`dp` 函数的定义如下: + +**若 `dp(s, i, p, j) = true`,则表示 `s[i..]` 可以匹配 `p[j..]`;若 `dp(s, i, p, j) = false`,则表示 `s[i..]` 无法匹配 `p[j..]`**。 + +根据这个定义,我们想要的答案就是 `i = 0, j = 0` 时 `dp` 函数的结果,所以可以这样使用这个 `dp` 函数: + +```java +boolean isMatch(String s, String p) { + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0); +} ``` -星号前面的那个字符到底要重复几次呢?这需要计算机暴力穷举来算,假设重复 N 次吧。前文多次强调过,写递归的技巧是管好当下,之后的事抛给递归。具体到这里,不管 N 是多少,当前的选择只有两个:匹配 0 次、匹配 1 次。所以可以这样处理: +可以根据之前的代码写出 `dp` 函数的主要逻辑: + +```java +// dp 函数的核心逻辑(伪码) +// 定义:函数返回 s[i..] 是否能匹配 p[j..] +boolean dp(String s, int i, String p, int j) { + if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { + // 匹配 + if (j < p.length() - 1 && p.charAt(j + 1) == '*') { + // 1.1 通配符匹配 0 次或多次 + return dp(s, i, p, j + 2) || dp(s, i + 1, p, j); + } else { + // 1.2 常规匹配 1 次 + return dp(s, i + 1, p, j + 1); + } + } else { + // 不匹配 + if (j < p.length() - 1 && p.charAt(j + 1) == '*') { + // 2.1 通配符匹配 0 次 + return dp(s, i, p, j + 2); + } else { + // 2.2 无法继续匹配 + return false; + } + } +} +``` + +**根据 `dp` 函数的定义**,这几种情况都很好解释: + +**1.1 通配符匹配 0 次或多次** + +将 `j` 加 2,`i` 不变,含义就是直接跳过 `p[j]` 和之后的通配符,即通配符匹配 0 次。 + +即便 `s[i] == p[j]`,依然可能出现这种情况,如下图: + +![](https://labuladong.online/algo/images/regex/5.jpeg) + +将 `i` 加 1,`j` 不变,含义就是 `p[j]` 匹配了 `s[i]`,但 `p[j]` 还可以继续匹配,即通配符匹配多次的情况: + +![](https://labuladong.online/algo/images/regex/2.jpeg) + +两种情况只要有一种可以完成匹配即可,所以对上面两种情况求或运算。 + +**1.2 常规匹配 1 次** + +由于这个条件分支是无 `*` 的常规匹配,那么如果 `s[i] == p[j]`,就是 `i` 和 `j` 分别加一: + +![](https://labuladong.online/algo/images/regex/3.jpeg) -```py -if len(pattern) >= 2 and pattern[1] == '*': - return isMatch(text, pattern[2:]) or \ - first_match and isMatch(text[1:], pattern) -# 解释:如果发现有字符和 '*' 结合, - # 或者匹配该字符 0 次,然后跳过该字符和 '*' - # 或者当 pattern[0] 和 text[0] 匹配后,移动 text +**2.1 通配符匹配 0 次** + +类似情况 1.1,将 `j` 加 2,`i` 不变: + +![](https://labuladong.online/algo/images/regex/1.jpeg) + +**2.2 如果没有 `*` 通配符,也无法匹配,那只能说明匹配失败了** + +![](https://labuladong.online/algo/images/regex/4.jpeg) + +看图理解应该很容易了,现在可以思考一下 `dp` 函数的 base case: + +**一个 base case 是 `j == p.length()` 时**,按照 `dp` 函数的定义,这意味着模式串 `p` 已经被匹配完了,那么应该看看文本串 `s` 匹配到哪里了,如果 `s` 也恰好被匹配完,则说明匹配成功: + + + + + +```java +if (j == p.length()) { + return i == s.length(); +} ``` -可以看到,我们是通过保留 pattern 中的「\*」,同时向后推移 text,来实现「*」将字符重复匹配多次的功能。举个简单的例子就能理解这个逻辑了。假设 `pattern = a*`, `text = aaa`,画个图看看匹配过程: - -![example](../pictures/%E6%AD%A3%E5%88%99/example.png) - -至此,正则表达式算法就基本完成了, - -### 四、动态规划 - -我选择使用「备忘录」递归的方法来降低复杂度。有了暴力解法,优化的过程及其简单,就是使用两个变量 i, j 记录当前匹配到的位置,从而避免使用子字符串切片,并且将 i, j 存入备忘录,避免重复计算即可。 - -我将暴力解法和优化解法放在一起,方便你对比,你可以发现优化解法无非就是把暴力解法「翻译」了一遍,加了个 memo 作为备忘录,仅此而已。 - -```py -# 带备忘录的递归 -def isMatch(text, pattern) -> bool: - memo = dict() # 备忘录 - def dp(i, j): - if (i, j) in memo: return memo[(i, j)] - if j == len(pattern): return i == len(text) - - first = i < len(text) and pattern[j] in {text[i], '.'} - - if j <= len(pattern) - 2 and pattern[j + 1] == '*': - ans = dp(i, j + 2) or \ - first and dp(i + 1, j) - else: - ans = first and dp(i + 1, j + 1) - - memo[(i, j)] = ans - return ans - - return dp(0, 0) - -# 暴力递归 -def isMatch(text, pattern) -> bool: - if not pattern: return not text - - first = bool(text) and pattern[0] in {text[0], '.'} - - if len(pattern) >= 2 and pattern[1] == '*': - return isMatch(text, pattern[2:]) or \ - first and isMatch(text[1:], pattern) - else: - return first and isMatch(text[1:], pattern[1:]) + + + +**另一个 base case 是 `i == s.length()` 时**,按照 `dp` 函数的定义,这种情况意味着文本串 `s` 已经全部被匹配了,那么是不是只要简单地检查一下 `p` 是否也匹配完就行了呢? + + + + + +```java +if (i == s.length()) { + // 这样行吗? + return j == p.length(); +} ``` -**有的读者也许会问,你怎么知道这个问题是个动态规划问题呢,你怎么知道它就存在「重叠子问题」呢,这似乎不容易看出来呀?** -解答这个问题,最直观的应该是随便假设一个输入,然后画递归树,肯定是可以发现相同节点的。这属于定量分析,其实不用这么麻烦,下面我来教你定性分析,一眼就能看出「重叠子问题」性质。 -先拿最简单的斐波那契数列举例,我们抽象出递归算法的框架: +**这是不正确的,此时并不能根据 `j` 是否等于 `p.length()` 来判断是否完成匹配,只要 `p[j..]` 能够匹配空串,就可以算完成匹配**。比如说 `s = "a", p = "ab*c*"`,当 `i` 走到 `s` 末尾的时候,`j` 并没有走到 `p` 的末尾,但是 `p` 依然可以匹配 `s`。 + +所以我们可以写出如下代码: + +```java +int m = s.length(), n = p.length(); + +if (i == s.length()) { + // 如果能匹配空串,一定是字符和 * 成对儿出现 + if ((n - j) % 2 == 1) { + return false; + } + // 检查是否为 x*y*z* 这种形式 + for (; j + 1 < p.length(); j += 2) { + if (p.charAt(j + 1) != '*') { + return false; + } + } + return true; +} +``` + +根据以上思路,就可以写出完整的代码: + +```java +class Solution { + // 备忘录 + int[][] memo; + + public boolean isMatch(String s, String p) { + int m = s.length(), n = p.length(); + memo = new int[m][n]; + for (int[] row : memo) { + Arrays.fill(row, -1); + } + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0); + } -```py -def fib(n): - fib(n - 1) #1 - fib(n - 2) #2 + // 计算 p[j..] 是否匹配 s[i..] + boolean dp(String s, int i, String p, int j) { + int m = s.length(), n = p.length(); + // base case + if (j == n) { + return i == m; + } + if (i == m) { + if ((n - j) % 2 == 1) { + return false; + } + for (; j + 1 < n; j += 2) { + if (p.charAt(j + 1) != '*') { + return false; + } + } + return true; + } + + // 查备忘录,防止重复计算 + if (memo[i][j] != -1) { + return memo[i][j] == 1; + } + + boolean res = false; + + if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2) || dp(s, i + 1, p, j); + } else { + res = dp(s, i + 1, p, j + 1); + } + } else { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2); + } else { + res = false; + } + } + // 将当前结果记入备忘录 + memo[i][j] = res ? 1 : 0; + return res; + } +} ``` -看着这个框架,请问原问题 f(n) 如何触达子问题 f(n - 2) ?有两种路径,一是 f(n) -> #1 -> #1, 二是 f(n) -> #2。前者经过两次递归,后者进过一次递归而已。两条不同的计算路径都到达了同一个问题,这就是「重叠子问题」,而且可以肯定的是,**只要你发现一条重复路径,这样的重复路径一定存在千万条,意味着巨量子问题重叠。** -同理,对于本问题,我们依然先抽象出算法框架: +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +代码中用了一个哈希表 `memo` 消除重叠子问题。如何一眼看出重叠子问题?可以参考前文 [动态规划系列答疑](https://labuladong.online/algo/dynamic-programming/faq-summary/) 中所讲的技巧,抽出正则表达算法的递归框架: -```py -def dp(i, j): - dp(i, j + 2) #1 - dp(i + 1, j) #2 - dp(i + 1, j + 1) #3 +```java +boolean dp(String s, int i, String p, int j) { + dp(s, i, p, j + 2); // 1 + dp(s, i + 1, p, j); // 2 + dp(s, i + 1, p, j + 1); // 3 + dp(s, i, p, j + 2); // 4 +} ``` -提出类似的问题,请问如何从原问题 dp(i, j) 触达子问题 dp(i + 2, j + 2) ?至少有两种路径,一是 dp(i, j) -> #3 -> #3,二是 dp(i, j) -> #1 -> #2 -> #2。因此,本问题一定存在重叠子问题,一定需要动态规划的优化技巧来处理。 +如果让你从 `dp(s, i, p, j)` 得到 `dp(s, i+2, p, j+2)`,至少有两条路径:`1 -> 2 -> 2` 和 `3 -> 3`。这就说明 `(i+2, j+2)` 这个状态的计算必然存在重复,也就说明存在重叠子问题,我们需要用备忘录消除重叠子问题,提高效率。 + +动态规划的时间复杂度为「状态的总数」*「每次递归花费的时间」,本题中状态的总数当然就是 `i` 和 `j` 的组合,也就是 `M * N`(`M` 为 `s` 的长度,`N` 为 `p` 的长度);递归函数 `dp` 中没有循环(base case 中的不考虑,因为 base case 的触发次数有限),所以一次递归花费的时间为常数。二者相乘,总的时间复杂度为 $O(MN)$。 + +空间复杂度很简单,就是备忘录 `memo` 的大小,即 $O(MN)$。 + + + + + + + +
+
+引用本文的文章 + + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + +

+ + + + +
+
+引用本文的题目 -### 五、最后总结 +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: -通过本文,你深入理解了正则表达式的两种常用通配符的算法实现。其实点号「.」的实现及其简单,关键是星号「*」的实现需要用到动态规划技巧,稍微复杂些,但是也架不住我们对问题的层层拆解,逐个击破。另外,你掌握了一种快速分析「重叠子问题」性质的技巧,可以快速判断一个问题是否可以使用动态规划套路解决。 +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer 19. 正则表达式匹配](https://leetcode.cn/problems/zheng-ze-biao-da-shi-pi-pei-lcof/?show=1) | 🔴 | -回顾整个解题过程,你应该能够体会到算法设计的流程:从简单的类似问题入手,给基本的框架逐渐组装新的逻辑,最终成为一个比较复杂、精巧的算法。所以说,读者不必畏惧一些比较复杂的算法问题,多思考多类比,再高大上的算法在你眼里也不过一个脆皮。 +
+
-如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~ +**_____________** -[上一篇:动态规划之四键键盘](../动态规划系列/动态规划之四键键盘.md) -[下一篇:最长公共子序列](../动态规划系列/最长公共子序列.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" index ccb36f4561..ac5a4f7939 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" @@ -1,194 +1,428 @@ # 动态规划设计:最长递增子序列 -很多读者反应,就算看了前文[动态规划详解](动态规划详解进阶.md),了解了动态规划的套路,也不会写状态转移方程,没有思路,怎么办?本文就借助「最长递增子序列」来讲一种设计动态规划的通用技巧:数学归纳思想。 -最长递增子序列(Longest Increasing Subsequence,简写 LIS)是比较经典的一个问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2),我们借这个问题来由浅入深讲解如何写动态规划。比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过一种简单的纸牌游戏来辅助理解这种巧妙的解法。 -先看一下题目,很容易理解: +![](https://labuladong.online/algo/images/souyisou1.png) -![title](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/title.png) +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。下面先来一步一步设计动态规划算法解决这个问题。 -### 一、动态规划解法 + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [300. Longest Increasing Subsequence](https://leetcode.com/problems/longest-increasing-subsequence/) | [300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) | 🟠 | +| [354. Russian Doll Envelopes](https://leetcode.com/problems/russian-doll-envelopes/) | [354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + + +也许有读者看了前文 [动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/),学会了动态规划的套路:找到了问题的「状态」,明确了 `dp` 数组/函数的含义,定义了 base case;但是不知道如何确定「选择」,也就是找不到状态转移的关系,依然写不出动态规划解法,怎么办? + +不要担心,动态规划的难点本来就在于寻找正确的状态转移方程,本文就借助经典的「最长递增子序列问题」来讲一讲设计动态规划的通用技巧:**数学归纳思想**。 + +最长递增子序列(Longest Increasing Subsequence,简写 LIS)是非常经典的一个算法问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2),我们借这个问题来由浅入深讲解如何找状态转移方程,如何写出动态规划解法。比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过一种简单的纸牌游戏来辅助理解这种巧妙的解法。 + +力扣第 300 题「最长递增子序列」就是这个问题: + + + + + + + +```java +// 函数签名 +int lengthOfLIS(int[] nums); +``` + +比如说输入 `nums=[10,9,2,5,3,7,101,18]`,其中最长的递增子序列是 `[2,3,7,101]`,所以算法的输出应该是 4。 + +注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。下面先来设计动态规划算法解决这个问题。 + +## 一、动态规划解法 动态规划的核心设计思想是数学归纳法。 -相信大家对数学归纳法都不陌生,高中就学过,而且思路很简单。比如我们想证明一个数学结论,那么我们先假设这个结论在 $k [!NOTE] +> 为什么这样定义呢?这是解决子序列问题的一个套路,后文 [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) 总结了几种常见套路。你读完本章所有的动态规划问题,就会发现 `dp` 数组的定义方法也就那几种。 -![1](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/1.jpeg) +根据这个定义,我们就可以推出 base case:`dp[i]` 初始值为 1,因为以 `nums[i]` 结尾的最长递增子序列起码要包含它自己。 +举两个例子: -![2](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/2.jpeg) +![](https://labuladong.online/algo/images/lis/8.jpeg) -算法演进的过程是这样的,: +这个 GIF 展示了算法演进的过程: -![gif1](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/gif1.gif) +![](https://labuladong.online/algo/images/lis/gif1.gif) 根据这个定义,我们的最终结果(子序列的最大长度)应该是 dp 数组中的最大值。 + + + + ```java int res = 0; -for (int i = 0; i < dp.size(); i++) { +for (int i = 0; i < dp.length; i++) { res = Math.max(res, dp[i]); } return res; ``` -读者也许会问,刚才这个过程中每个 dp[i] 的结果是我们肉眼看出来的,我们应该怎么设计算法逻辑来正确计算每个 dp[i] 呢? -这就是动态规划的重头戏了,要思考如何进行状态转移,这里就可以使用数学归纳的思想: -我们已经知道了 $dp[0...4]$ 的所有结果,我们如何通过这些已知结果推出 $dp[5]$ 呢? +读者也许会问,刚才的算法演进过程中每个 `dp[i]` 的结果是我们肉眼看出来的,我们应该怎么设计算法逻辑来正确计算每个 `dp[i]` 呢? + +这就是动态规划的重头戏,如何设计算法逻辑进行状态转移,才能正确运行呢?这里需要使用数学归纳的思想: + +**假设我们已经知道了 `dp[0..4]` 的所有结果,我们如何通过这些已知结果推出 `dp[5]` 呢**? + +![](https://labuladong.online/algo/images/lis/6.jpeg) + +根据刚才我们对 `dp` 数组的定义,现在想求 `dp[5]` 的值,也就是想求以 `nums[5]` 为结尾的最长递增子序列。 + +**`nums[5] = 3`,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度加一**。 -![3](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/3.jpeg) +`nums[5]` 前面有哪些元素小于 `nums[5]`?这个好算,用 for 循环比较一波就能把这些元素找出来。 -根据刚才我们对 dp 数组的定义,现在想求 dp[5] 的值,也就是想求以 nums[5] 为结尾的最长递增子序列。 +以这些元素为结尾的最长递增子序列的长度是多少?回顾一下我们对 `dp` 数组的定义,它记录的正是以每个元素为末尾的最长递增子序列的长度。 -nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。 +以我们举的例子来说,`nums[0]` 和 `nums[4]` 都是小于 `nums[5]` 的,然后对比 `dp[0]` 和 `dp[4]` 的值,我们让 `nums[5]` 和更长的递增子序列结合,得出 `dp[5] = 3`: -当然,可能形成很多种新的子序列,但是我们只要最长的,把最长子序列的长度作为 dp[5] 的值即可。 -![gif2](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/gif2.gif) + + + +![](https://labuladong.online/algo/images/lis/7.jpeg) ```java for (int j = 0; j < i; j++) { - if (nums[i] > nums[j]) + if (nums[i] > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1); + } } ``` -这段代码的逻辑就可以算出 dp[5]。到这里,这道算法题我们就基本做完了。读者也许会问,我们刚才只是算了 dp[5] 呀,dp[4], dp[3] 这些怎么算呢? -类似数学归纳法,你已经可以算出 dp[5] 了,其他的就都可以算出来: + +当 `i = 5` 时,这段代码的逻辑就可以算出 `dp[5]`。其实到这里,这道算法题我们就基本做完了。 + +读者也许会问,我们刚才只是算了 `dp[5]` 呀,`dp[4]`, `dp[3]` 这些怎么算呢?类似数学归纳法,你已经可以算出 `dp[5]` 了,其他的就都可以算出来: + + + + ```java for (int i = 0; i < nums.length; i++) { for (int j = 0; j < i; j++) { - if (nums[i] > nums[j]) + // 寻找 nums[0..i-1] 中比 nums[i] 小的元素 + if (nums[i] > nums[j]) { + // 把 nums[i] 接在后面,即可形成长度为 dp[j] + 1, + // 且以 nums[i] 为结尾的递增子序列 dp[i] = Math.max(dp[i], dp[j] + 1); + } } } ``` -还有一个细节问题,dp 数组应该全部初始化为 1,因为子序列最少也要包含自己,所以长度最小为 1。下面我们看一下完整代码: + + +结合我们刚才说的 base case,下面我们看一下完整代码: ```java -public int lengthOfLIS(int[] nums) { - int[] dp = new int[nums.length]; - // dp 数组全都初始化为 1 - Arrays.fill(dp, 1); - for (int i = 0; i < nums.length; i++) { - for (int j = 0; j < i; j++) { - if (nums[i] > nums[j]) - dp[i] = Math.max(dp[i], dp[j] + 1); +class Solution { + public int lengthOfLIS(int[] nums) { + // 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度 + int[] dp = new int[nums.length]; + // base case:dp 数组全都初始化为 1 + Arrays.fill(dp, 1); + for (int i = 0; i < nums.length; i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } } + + int res = 0; + for (int i = 0; i < dp.length; i++) { + res = Math.max(res, dp[i]); + } + return res; } - - int res = 0; - for (int i = 0; i < dp.length; i++) { - res = Math.max(res, dp[i]); - } - return res; } ``` -至此,这道题就解决了,时间复杂度 O(N^2)。总结一下动态规划的设计流程: -首先明确 dp 数组所存数据的含义。这步很重要,如果不得当或者不够清晰,会阻碍之后的步骤。 +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +至此,这道题就解决了,时间复杂度 $O(N^2)$。总结一下如何找到动态规划的状态转移关系: + +1、明确 `dp` 数组的定义。这一步对于任何动态规划问题都很重要,如果不得当或者不够清晰,会阻碍之后的步骤。 + +2、根据 `dp` 数组的定义,运用数学归纳法的思想,假设 `dp[0...i-1]` 都已知,想办法求出 `dp[i]`,一旦这一步完成,整个题目基本就解决了。 + +但如果无法完成这一步,很可能就是 `dp` 数组的定义不够恰当,需要重新定义 `dp` 数组的含义;或者可能是 `dp` 数组存储的信息还不够,不足以推出下一步的答案,需要把 `dp` 数组扩大成二维数组甚至三维数组。 + +目前的解法是标准的动态规划,但对最长递增子序列问题来说,这个解法不是最优的,可能无法通过所有测试用例了,下面讲讲更高效的解法。 -然后根据 dp 数组的定义,运用数学归纳法的思想,假设 $dp[0...i-1]$ 都已知,想办法求出 $dp[i]$,一旦这一步完成,整个题目基本就解决了。 -但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。 -最后想一想问题的 base case 是什么,以此来初始化 dp 数组,以保证算法正确运行。 -### 二、二分查找解法 -这个解法的时间复杂度会将为 O(NlogN),但是说实话,正常人基本想不到这种解法(也许玩过某些纸牌游戏的人可以想出来)。所以如果大家了解一下就好,正常情况下能够给出动态规划解法就已经很不错了。 + + +## 二、二分查找解法 + +这个解法的时间复杂度为 $O(NlogN)$,但是说实话,正常人基本想不到这种解法(也许玩过某些纸牌游戏的人可以想出来)。所以大家了解一下就好,正常情况下能够给出动态规划解法就已经很不错了。 根据题目的意思,我都很难想象这个问题竟然能和二分查找扯上关系。其实最长递增子序列和一种叫做 patience game 的纸牌游戏有关,甚至有一种排序方法就叫做 patience sorting(耐心排序)。 -为了简单期间,后文跳过所有数学证明,通过一个简化的例子来理解一下思路。 +为了简单起见,后文跳过所有数学证明,通过一个简化的例子来理解一下算法思路。 首先,给你一排扑克牌,我们像遍历数组那样从左到右一张一张处理这些扑克牌,最终要把这些牌分成若干堆。 -![poker1](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/poker1.jpeg) +![](https://labuladong.online/algo/images/lis/poker1.jpeg) + + + + + -处理这些扑克牌要遵循以下规则: -只能把点数小的牌压到点数比它大的牌上。如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去。如果当前牌有多个堆可供选择,则选择最左边的堆放置。 +**处理这些扑克牌要遵循以下规则**: -比如说上述的扑克牌最终会被分成这样 5 堆(我们认为 A 的值是最大的,而不是 1)。 +只能把点数小的牌压到点数比它大的牌上;如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去;如果当前牌有多个堆可供选择,则选择最左边的那一堆放置。 -![poker2](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/poker2.jpeg) +比如说上述的扑克牌最终会被分成这样 5 堆(我们认为纸牌 A 的牌面是最大的,纸牌 2 的牌面是最小的)。 + +![](https://labuladong.online/algo/images/lis/poker2.jpeg) 为什么遇到多个可选择堆的时候要放到最左边的堆上呢?因为这样可以保证牌堆顶的牌有序(2, 4, 7, 8, Q),证明略。 -![poker3](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/poker3.jpeg) +![](https://labuladong.online/algo/images/lis/poker3.jpeg) 按照上述规则执行,可以算出最长递增子序列,牌的堆数就是最长递增子序列的长度,证明略。 -![LIS](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/poker4.jpeg) +![](https://labuladong.online/algo/images/lis/poker4.jpeg) -我们只要把处理扑克牌的过程编程写出来即可。每次处理一张扑克牌不是要找一个合适的牌堆顶来放吗,牌堆顶的牌不是有序吗,这就能用到二分查找了:用二分查找来搜索当前牌应放置的位置。 +我们只要把处理扑克牌的过程编程写出来即可。每次处理一张扑克牌不是要找一个合适的牌堆顶来放吗,牌堆顶的牌不是**有序**吗,这就能用到二分查找了:用二分查找来搜索当前牌应放置的位置。 -PS:旧文[二分查找算法详解](../算法思维系列/二分查找详解.md)详细介绍了二分查找的细节及变体,这里就完美应用上了。如果没读过强烈建议阅读。 +> [!TIP] +> 前文 [二分查找算法详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) 详细介绍了二分查找的细节及变体,这里就完美应用上了,如果没读过强烈建议阅读。 ```java -public int lengthOfLIS(int[] nums) { - int[] top = new int[nums.length]; - // 牌堆数初始化为 0 - int piles = 0; - for (int i = 0; i < nums.length; i++) { - // 要处理的扑克牌 - int poker = nums[i]; - - /***** 搜索左侧边界的二分查找 *****/ - int left = 0, right = piles; - while (left < right) { - int mid = (left + right) / 2; - if (top[mid] > poker) { - right = mid; - } else if (top[mid] < poker) { - left = mid + 1; - } else { - right = mid; +class Solution { + public int lengthOfLIS(int[] nums) { + int[] top = new int[nums.length]; + // 牌堆数初始化为 0 + int piles = 0; + for (int i = 0; i < nums.length; i++) { + // 要处理的扑克牌 + int poker = nums[i]; + + // ***** 搜索左侧边界的二分查找 ***** + int left = 0, right = piles; + while (left < right) { + int mid = (left + right) / 2; + if (top[mid] > poker) { + right = mid; + } else if (top[mid] < poker) { + left = mid + 1; + } else { + right = mid; + } } + // ********************************* + + // 没找到合适的牌堆,新建一堆 + if (left == piles) piles++; + // 把这张牌放到牌堆顶 + top[left] = poker; } - /*********************************/ - - // 没找到合适的牌堆,新建一堆 - if (left == piles) piles++; - // 把这张牌放到牌堆顶 - top[left] = poker; + // 牌堆数就是 LIS 长度 + return piles; } - // 牌堆数就是 LIS 长度 - return piles; } ``` + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + 至此,二分查找的解法也讲解完毕。 这个解法确实很难想到。首先涉及数学证明,谁能想到按照这些规则执行,就能得到最长递增子序列呢?其次还有二分查找的运用,要是对二分查找的细节不清楚,给了思路也很难写对。 所以,这个方法作为思维拓展好了。但动态规划的设计方法应该完全理解:假设之前的答案已知,利用数学归纳的思想正确进行状态的推演转移,最终得到答案。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:动态规划答疑篇](../动态规划系列/最优子结构.md) -[下一篇:编辑距离](../动态规划系列/编辑距离.md) -[目录](../README.md#目录) \ No newline at end of file + +## 三、拓展到二维 + +我们看一个经常出现在生活中的有趣问题,力扣第 354 题「俄罗斯套娃信封问题」,先看下题目: + + + +**这道题目其实是最长递增子序列的一个变种,因为每次合法的嵌套是大的套小的,相当于在二维平面中找一个最长递增的子序列,其长度就是最多能嵌套的信封个数**。 + +前面说的标准 LIS 算法只能在一维数组中寻找最长子序列,而我们的信封是由 `(w, h)` 这样的二维数对形式表示的,如何把 LIS 算法运用过来呢? + +![](https://labuladong.online/algo/images/nest-envelope/0.jpg) + +读者也许会想,通过 `w × h` 计算面积,然后对面积进行标准的 LIS 算法。但是稍加思考就会发现这样不行,比如 `1 × 10` 大于 `3 × 3`,但是显然这样的两个信封是无法互相嵌套的。 + + + + + + + +这道题的解法比较巧妙: + +**先对宽度 `w` 进行升序排序,如果遇到 `w` 相同的情况,则按照高度 `h` 降序排序;之后把所有的 `h` 作为一个数组,在这个数组上计算 LIS 的长度就是答案**。 + +画个图理解一下,先对这些数对进行排序: + +![](https://labuladong.online/algo/images/nest-envelope/1.jpg) + +然后在 `h` 上寻找最长递增子序列,这个子序列就是最优的嵌套方案: + +![](https://labuladong.online/algo/images/nest-envelope/2.jpg) + +**那么为什么这样就可以找到可以互相嵌套的信封序列呢**?稍微思考一下就明白了: + +首先,对宽度 `w` 从小到大排序,确保了 `w` 这个维度可以互相嵌套,所以我们只需要专注高度 `h` 这个维度能够互相嵌套即可。 + +其次,两个 `w` 相同的信封不能相互包含,所以对于宽度 `w` 相同的信封,对高度 `h` 进行降序排序,保证二维 LIS 中不存在多个 `w` 相同的信封(因为题目说了长宽相同也无法嵌套)。 + +下面看解法代码: + +```java +class Solution { + // envelopes = [[w, h], [w, h]...] + public int maxEnvelopes(int[][] envelopes) { + int n = envelopes.length; + // 按宽度升序排列,如果宽度一样,则按高度降序排列 + Arrays.sort(envelopes, (int[] a, int[] b) -> { + return a[0] == b[0] ? + b[1] - a[1] : a[0] - b[0]; + }); + // 对高度数组寻找 LIS + int[] height = new int[n]; + for (int i = 0; i < n; i++) + height[i] = envelopes[i][1]; + + return lengthOfLIS(height); + } + + int lengthOfLIS(int[] nums) { + // 见前文 + } +} +``` + + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + +为了复用之前的函数,我将代码分为了两个函数,你也可以合并代码,节省下 `height` 数组的空间。 + +由于增加了测试用例,这里必须使用二分搜索版的 `lengthOfLIS` 函数才能通过所有测试用例。这样的话算法的时间复杂度为 $O(NlogN)$,因为排序和计算 LIS 各需要 $O(NlogN)$ 的时间,加到一起还是 $O(NlogN)$;空间复杂度为 $O(N)$,因为计算 LIS 的函数中需要一个 `top` 数组。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】单调队列的通用实现及经典习题](https://labuladong.online/algo/problem-set/monotonic-queue/) + - [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) + - [动态规划穷举的两种视角](https://labuladong.online/algo/dynamic-programming/two-views-of-dp/) + - [动态规划设计:最大子数组](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1425. Constrained Subsequence Sum](https://leetcode.com/problems/constrained-subsequence-sum/?show=1) | [1425. 带限制的子序列和](https://leetcode.cn/problems/constrained-subsequence-sum/?show=1) | 🔴 | +| [256. Paint House](https://leetcode.com/problems/paint-house/?show=1)🔒 | [256. 粉刷房子](https://leetcode.cn/problems/paint-house/?show=1)🔒 | 🟠 | +| [368. Largest Divisible Subset](https://leetcode.com/problems/largest-divisible-subset/?show=1) | [368. 最大整除子集](https://leetcode.cn/problems/largest-divisible-subset/?show=1) | 🟠 | +| - | [剑指 Offer II 091. 粉刷房子](https://leetcode.cn/problems/JEj789/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" index 6fc27cef57..95feea2b83 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" @@ -1,162 +1,324 @@ -# 动态规划详解 +# 动态规划解题套路框架 -这篇文章是我们号半年前一篇 200 多赞赏的成名之作「动态规划详解」的进阶版。由于账号迁移的原因,旧文无法被搜索到,所以我润色了本文,并添加了更多干货内容,希望本文成为解决动态规划的一部「指导方针」。 -再说句题外话,我们的公众号开号至今写了起码十几篇文章拆解动态规划问题,我都整理到了公众号菜单的「文章目录」中,**它们都提到了动态规划的解题框架思维,本文就系统总结一下**。这段时间本人也从非科班小白成长到刷通半个 LeetCode,所以我总结的套路可能不适合各路大神,但是应该适合大众,毕竟我自己也是一路摸爬滚打过来的。 -算法技巧就那几个套路,如果你心里有数,就会轻松很多,本文就来扒一扒动态规划的裤子,形成一套解决这类问题的思维框架。废话不多说了,上干货。 +![](https://labuladong.online/algo/images/souyisou1.png) -**动态规划问题的一般形式就是求最值**。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求**最长**递增子序列呀,**最小**编辑距离呀等等。 +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [322. Coin Change](https://leetcode.com/problems/coin-change/) | [322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | 🟠 | +| [509. Fibonacci Number](https://leetcode.com/problems/fibonacci-number/) | [509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树的遍历框架](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) +> - [多叉树结构及遍历框架](https://labuladong.online/algo/data-structure-basic/n-ary-tree-traverse-basic/) + +> tip:本文有视频版:[动态规划框架套路详解](https://www.bilibili.com/video/BV1XV411Y7oE)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +这篇文章是我多年前一篇 200 多赞赏的 [动态规划详解](https://mp.weixin.qq.com/s/1V3aHVonWBEXlNUvK3S28w) 的进阶版,我添加了更多干货内容,希望本文成为解决动态规划的一部「指导方针」。 + +动态规划问题(Dynamic Programming)应该是很多读者头疼的,不过这类问题也是最具有技巧性,最有意思的。本书使用了整整一个章节专门来写这个算法,动态规划的重要性也可见一斑。 + +本文解决几个问题: + +动态规划是什么?解决动态规划问题有什么技巧?如何学习动态规划? + +刷题刷多了就会发现,算法技巧就那几个套路,我们后续的动态规划系列章节,都在使用本文的解题框架思维,如果你心里有数,就会轻松很多。所以本文放在第一章,希望能够成为解决动态规划问题的一部指导方针,下面上干货。 + +首先,**动态规划问题的一般形式就是求最值**。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。 既然是要求最值,核心问题是什么呢?**求解动态规划的核心问题是穷举**。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。 -动态规划就这么简单,就是穷举就完事了?我看到的动态规划问题都很难啊! +动态规划这么简单,就是穷举就完事了?我看到的动态规划问题都很难啊! + -首先,动态规划的穷举有点特别,因为这类问题**存在「重叠子问题」**,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。 -而且,动态规划问题一定会**具备「最优子结构」**,才能通过子问题的最值得到原问题的最值。 -另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程**」才能正确地穷举。 -以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。具体什么意思等会会举例详解,但是在实际的算法问题中,**写出状态转移方程是最困难的**,这也就是为什么很多朋友觉得动态规划问题困难的原因,我来提供我研究出来的一个思维框架,辅助你思考状态转移方程: -明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。 -下面通过斐波那契数列问题和凑零钱问题来详解动态规划的基本原理。前者主要是让你明白什么是重叠子问题(斐波那契数列严格来说不是动态规划问题),后者主要举集中于如何列出状态转移方程。 +首先,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,需要你熟练掌握递归思维,只有列出**正确的「状态转移方程」**,才能正确地穷举。而且,你需要判断算法问题是否**具备「最优子结构」**,是否能够通过子问题的最值得到原问题的最值。另外,动态规划问题**存在「重叠子问题」**,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。 -请读者不要嫌弃这个例子简单,**只有简单的例子才能让你把精力充分集中在算法背后的通用思想和技巧上,而不会被那些隐晦的细节问题搞的莫名其妙**。想要困难的例子,历史文章里有的是。 +以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。具体什么意思等会会举例详解,但是在实际的算法问题中,写出状态转移方程是最困难的,这也就是为什么很多朋友觉得动态规划问题困难的原因,我来提供我总结的一个思维框架,辅助你思考状态转移方程: -### 一、斐波那契数列 +**明确「状态」-> 明确「选择」 -> 定义 `dp` 数组/函数的含义**。 -**1、暴力递归** +按上面的套路走,最后的解法代码就会是如下的框架: + + + + + + + +```python +# 自顶向下递归的动态规划 +def dp(状态1, 状态2, ...): + for 选择 in 所有可能的选择: + # 此时的状态已经因为做了选择而改变 + result = 求最值(result, dp(状态1, 状态2, ...)) + return result + +# 自底向上迭代的动态规划 +# 初始化 base case +dp[0][0][...] = base case +# 进行状态转移 +for 状态1 in 状态1的所有取值: + for 状态2 in 状态2的所有取值: + for ... + dp[状态1][状态2][...] = 求最值(选择1,选择2...) +``` + +下面通过斐波那契数列问题和凑零钱问题来详解动态规划的基本原理。前者主要是让你明白什么是重叠子问题(斐波那契数列没有求最值,所以严格来说不是动态规划问题),后者主要举集中于如何列出状态转移方程。 + +## 一、斐波那契数列 + +力扣第 509 题「斐波那契数」就是这个问题,请读者不要嫌弃这个例子简单,**只有简单的例子才能让你把精力充分集中在算法背后的通用思想和技巧上,而不会被那些隐晦的细节问题搞的莫名其妙**。想要困难的例子,接下来的动态规划系列里有的是。 + +### 暴力递归 斐波那契数列的数学形式就是递归的,写成代码就是这样: -```cpp +```java int fib(int N) { if (N == 1 || N == 2) return 1; return fib(N - 1) + fib(N - 2); } ``` -这个不用多说了,学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树。 -PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。 +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + +这个不用多说了,学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树: -![](../pictures/动态规划详解进阶/1.jpg) +![](https://labuladong.online/algo/images/dynamic-programming/1.jpg) + +> [!TIP] +> 但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。 这个递归树怎么理解?就是说想要计算原问题 `f(20)`,我就得先计算出子问题 `f(19)` 和 `f(18)`,然后要计算 `f(19)`,我就要先算出子问题 `f(18)` 和 `f(17)`,以此类推。最后遇到 `f(1)` 或者 `f(2)` 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。 -**递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。** +**递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间**。 -子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。 +首先计算子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。 -解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。 +然后计算解决一个子问题的时间,在本算法中,没有循环,只有 `f(n - 1) + f(n - 2)` 一个加法操作,时间为 O(1)。 -所以,这个算法的时间复杂度为 O(2^n),指数级别,爆炸。 +所以,这个算法的时间复杂度为二者相乘,即 O(2^n),指数级别,爆炸。 观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如 `f(18)` 被计算了两次,而且你可以看到,以 `f(18)` 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 `f(18)` 这一个节点被重复计算,所以这个算法及其低效。 这就是动态规划问题的第一个性质:**重叠子问题**。下面,我们想办法解决这个问题。 -**2、带备忘录的递归解法** + + + + + + +### 带备忘录的递归解法 明确了问题,其实就已经把问题解决了一半。即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。 一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。 -```cpp +```java int fib(int N) { - if (N < 1) return 0; // 备忘录全初始化为 0 - vector memo(N + 1, 0); - // 初始化最简情况 - return helper(memo, N); + int[] memo = new int[N + 1]; + // 进行带备忘录的递归 + return dp(memo, N); } - -int helper(vector& memo, int n) { - // base case - if (n == 1 || n == 2) return 1; - // 已经计算过 + +// 带着备忘录进行递归 +int dp(int[] memo, int n) { + // base case + if (n == 0 || n == 1) return n; + // 已经计算过,不用再计算了 if (memo[n] != 0) return memo[n]; - memo[n] = helper(memo, n - 1) + - helper(memo, n - 2); + memo[n] = dp(memo, n - 1) + dp(memo, n - 2); return memo[n]; } ``` + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + 现在,画出递归树,你就知道「备忘录」到底做了什么。 -![](../pictures/动态规划详解进阶/2.jpg) +![](https://labuladong.online/algo/images/dynamic-programming/2.jpg) 实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。 -![](../pictures/动态规划详解进阶/3.jpg) +![](https://labuladong.online/algo/images/dynamic-programming/3.jpg) -递归算法的时间复杂度怎么算?子问题个数乘以解决一个子问题需要的时间。 +**递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间**。 子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是 `f(1)`, `f(2)`, `f(3)` ... `f(20)`,数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。 解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。 -所以,本算法的时间复杂度是 O(n)。比起暴力算法,是降维打击。 +所以,本算法的时间复杂度是 O(n),比起暴力算法,是降维打击。 + + + + -至此,带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和迭代的动态规划已经差不多了,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。 -啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 `f(20)`,向下逐渐分解规模,直到 `f(1)` 和 `f(2)` 触底,然后逐层返回答案,这就叫「自顶向下」。 -啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 `f(1)` 和 `f(2)` 开始往上推,直到推到我们想要的答案 `f(20)`,这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。 +至此,带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和常见的动态规划解法已经差不多了,只不过这种解法是「自顶向下」进行「递归」求解,我们更常见的动态规划代码是「自底向上」进行「递推」求解。 -**3、dp 数组的迭代解法** +啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 `f(20)`,向下逐渐分解规模,直到 `f(1)` 和 `f(2)` 这两个 base case,然后逐层返回答案,这就叫「自顶向下」。 -有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,就叫做 DP table 吧,在这张表上完成「自底向上」的推算岂不美哉! +啥叫「自底向上」?反过来,我们直接从最底下、最简单、问题规模最小、已知结果的 `f(1)` 和 `f(2)`(base case)开始往上推,直到推到我们想要的答案 `f(20)`。这就是「递推」的思路,这也是动态规划一般都脱离了递归,而是由循环迭代完成计算的原因。 -```cpp +### `dp` 数组的迭代(递推)解法 + +有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,通常叫做 DP table,在这张表上完成「自底向上」的推算岂不美哉! + +```java int fib(int N) { - vector dp(N + 1, 0); + if (N == 0) return 0; + int[] dp = new int[N + 1]; // base case - dp[1] = dp[2] = 1; - for (int i = 3; i <= N; i++) + dp[0] = 0; dp[1] = 1; + // 状态转移 + for (int i = 2; i <= N; i++) { dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[N]; } ``` -![](../pictures/动态规划详解进阶/4.jpg) -画个图就很好理解了,而且你发现这个 DP table 特别像之前那个「剪枝」后的结果,只是反过来算而已。实际上,带备忘录的递归解法中的「备忘录」,最终完成后就是这个 DP table,所以说这两种解法其实是差不多的,大部分情况下,效率也基本相同。 +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + +画个图就很好理解了,而且你发现这个 DP table 特别像之前那个「剪枝」后的结果,只是反过来算而已: + +![](https://labuladong.online/algo/images/dynamic-programming/4.jpg) + +实际上,带备忘录的递归解法中的那个「备忘录」`memo` 数组,最终完成后就是这个解法中的 `dp` 数组,你对比一下可视化面板中两个算法执行的过程可以更直观地看出它俩的联系。 + +所以说自顶向下、自底向上两种解法本质其实是差不多的,大部分情况下,效率也基本相同。 + + + + + + + +### 拓展延伸 这里,引出「状态转移方程」这个名词,实际上就是描述问题结构的数学形式: -![](../pictures/动态规划详解进阶/fib.png) +![](https://labuladong.online/algo/images/dynamic-programming/fib.png) + +为啥叫「状态转移方程」?其实就是为了听起来高端。 + +`f(n)` 的函数参数会不断变化,所以你把参数 `n` 想做一个状态,这个状态 `n` 是由状态 `n - 1` 和状态 `n - 2` 转移(相加)而来,这就叫状态转移,仅此而已。 + +你会发现,上面的几种解法中的所有操作,例如 `return f(n - 1) + f(n - 2)`,`dp[i] = dp[i - 1] + dp[i - 2]`,以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。 + +可见列出「状态转移方程」的重要性,它是解决问题的核心,而且很容易发现,其实状态转移方程直接代表着暴力解法。 -为啥叫「状态转移方程」?为了听起来高端。你把 f(n) 想做一个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就叫状态转移,仅此而已。 +**千万不要看不起暴力解,动态规划问题最困难的就是写出这个暴力解,即状态转移方程**。 -你会发现,上面的几种解法中的所有操作,例如 return f(n - 1) + f(n - 2),dp[i] = dp[i - 1] + dp[i - 2],以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。可见列出「状态转移方程」的重要性,它是解决问题的核心。很容易发现,其实状态转移方程直接代表着暴力解法。 +只要写出暴力解,优化方法无非是用备忘录或者 DP table,再无奥妙可言。 -**千万不要看不起暴力解,动态规划问题最困难的就是写出状态转移方程**,即这个暴力解。优化方法无非是用备忘录或者 DP table,再无奥妙可言。 +这个例子的最后,讲一个细节优化。 -这个例子的最后,讲一个细节优化。细心的读者会发现,根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1): +细心的读者会发现,根据斐波那契数列的状态转移方程,当前状态 `n` 只和之前的 `n-1, n-2` 两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。 -```cpp +所以,可以进一步优化,把空间复杂度降为 O(1)。这也就是我们最常见的计算斐波那契数的算法: + +```java int fib(int n) { - if (n == 2 || n == 1) - return 1; - int prev = 1, curr = 1; - for (int i = 3; i <= n; i++) { - int sum = prev + curr; - prev = curr; - curr = sum; + if (n == 0 || n == 1) { + // base case + return n; } - return curr; + // 分别代表 dp[i - 1] 和 dp[i - 2] + int dp_i_1 = 1, dp_i_2 = 0; + for (int i = 2; i <= n; i++) { + // dp[i] = dp[i - 1] + dp[i - 2]; + int dp_i = dp_i_1 + dp_i_2; + // 滚动更新 + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i_1; } ``` -有人会问,动态规划的另一个重要特性「最优子结构」,怎么没有涉及?下面会涉及。斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值,以上旨在演示算法设计螺旋上升的过程。下面,看第二个例子,凑零钱问题。 -### 二、凑零钱问题 +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ -先看下题目:给你 `k` 种面值的硬币,面值分别为 `c1, c2 ... ck`,每种硬币的数量无限,再给一个总金额 `amount`,问你**最少**需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下: + +这一般是动态规划问题的最后一步优化,如果我们发现每次状态转移只需要 DP table 中的一部分,那么可以尝试缩小 DP table 的大小,只记录必要的数据,从而降低空间复杂度。 + +上述例子就相当于把 DP table 的大小从 `n` 缩小到 2,即把空间复杂度下降了一个量级。我会在后文 [对动态规划发动降维打击](https://labuladong.online/algo/dynamic-programming/space-optimization/) 进一步讲解这个压缩空间复杂度的技巧,一般来说用来把一个二维的 DP table 压缩成一维,即把空间复杂度从 O(n^2) 压缩到 O(n)。 + +有人会问,动态规划的另一个重要特性「最优子结构」,怎么没有涉及?下面会涉及。斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值,以上旨在说明重叠子问题的消除方法,演示得到最优解法逐步求精的过程。下面,看第二个例子,凑零钱问题。 + +## 二、凑零钱问题 + +这是力扣第 322 题「零钱兑换」: + +给你 `k` 种面值的硬币,面值分别为 `c1, c2 ... ck`,每种硬币的数量无限,再给一个总金额 `amount`,问你**最少**需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下: ```java // coins 中是可选硬币面值,amount 是目标金额 @@ -165,152 +327,329 @@ int coinChange(int[] coins, int amount); 比如说 `k = 3`,面值分别为 1,2,5,总金额 `amount = 11`。那么最少需要 3 枚硬币凑出,即 11 = 5 + 5 + 1。 -你认为计算机应该如何解决这个问题?显然,就是把所有肯能的凑硬币方法都穷举出来,然后找找看最少需要多少枚硬币。 +你认为计算机应该如何解决这个问题?显然,就是把所有可能的凑硬币方法都穷举出来,然后找找看最少需要多少枚硬币。 -**1、暴力递归** +### 暴力递归 首先,这个问题是动态规划问题,因为它具有「最优子结构」的。**要符合「最优子结构」,子问题间必须互相独立**。啥叫相互独立?你肯定不想看数学证明,我用一个直观的例子来讲解。 -比如说,你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。 +比如说,假设你考试,每门科目的成绩都是互相独立的。你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。 -得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,“每门科目考到最高”这些子问题是互相独立,互不干扰的。 +得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,「每门科目考到最高」这些子问题是互相独立,互不干扰的。 -但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,此消彼长。这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为子问题并不独立,语文数学成绩无法同时最优,所以最优子结构被破坏。 +但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,不能同时达到满分,数学分数高,语文分数就会降低,反之亦然。 -回到凑零钱问题,为什么说它符合最优子结构呢?比如你想求 `amount = 11` 时的最少硬币数(原问题),如果你知道凑出 `amount = 10` 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案,因为硬币的数量是没有限制的,子问题之间没有相互制,是互相独立的。 +这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为「每门科目考到最高」的子问题并不独立,语文数学成绩户互相影响,无法同时最优,所以最优子结构被破坏。 -那么,既然知道了这是个动态规划问题,就要思考**如何列出正确的状态转移方程**? +回到凑零钱问题,为什么说它符合最优子结构呢?假设你有面值为 `1, 2, 5` 的硬币,你想求 `amount = 11` 时的最少硬币数(原问题),如果你知道凑出 `amount = 10, 9, 6` 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 `1, 2, 5` 的硬币),求个最小值,就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。 -**先确定「状态」**,也就是原问题和子问题中变化的变量。由于硬币数量无限,所以唯一的状态就是目标金额 `amount`。 -**然后确定 `dp` 函数的定义**:当前的目标金额是 `n`,至少需要 `dp(n)` 个硬币凑出该金额。 -**然后确定「选择」并择优**,也就是对于每个状态,可以做出什么选择改变当前状态。具体到这个问题,无论当的目标金额是多少,选择就是从面额列表 `coins` 中选择一个硬币,然后目标金额就会减少: -```python -# 伪码框架 -def coinChange(coins: List[int], amount: int): - # 定义:要凑出金额 n,至少要 dp(n) 个硬币 - def dp(n): - # 做选择,选择需要硬币最少的那个结果 - for coin in coins: - res = min(res, 1 + dp(n - coin)) - return res - # 我们要求的问题是 dp(amount) - return dp(amount) + + + +> [!TIP] +> 关于最优子结构的问题,后文 [动态规划答疑篇](https://labuladong.online/algo/dynamic-programming/faq-summary/) 还会再举例探讨。 + +那么,既然知道了这是个动态规划问题,就要思考如何列出正确的状态转移方程? + +**1、确定「状态」,也就是原问题和子问题中会变化的变量**。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 `amount`。 + +**2、确定「选择」,也就是导致「状态」产生变化的行为**。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。 + +**3、明确 `dp` 函数/数组的定义**。我们这里讲的是自顶向下的解法,所以会有一个递归的 `dp` 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。 + +**所以我们可以这样定义 `dp` 函数:`dp(n)` 表示,输入一个目标金额 `n`,返回凑出目标金额 `n` 所需的最少硬币数量**。 + +那么根据这个定义,我们的最终答案就是 `dp(amount)` 的返回值。 + +搞清楚上面这几个关键点,解法的伪码就可以写出来了: + +```java +// 伪码框架 +int coinChange(int[] coins, int amount) { + // 题目要求的最终结果是 dp(amount) + return dp(coins, amount); +} + +// 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币 +int dp(int[] coins, int n) { + // 做选择,选择需要硬币最少的那个结果 + for (int coin : coins) { + res = min(res, 1 + dp(coins, n - coin)); + } + return res; +} ``` -**最后明确 base case**,显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1: +根据伪码,我们加上 base case 即可得到最终的答案。显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1: -```python -def coinChange(coins: List[int], amount: int): - - def dp(n): - # base case - if n == 0: return 0 - if n < 0: return -1 - # 求最小值,所以初始化为正无穷 - res = float('INF') - for coin in coins: - subproblem = dp(n - coin) - # 子问题无解,跳过 - if subproblem == -1: continue - res = min(res, 1 + subproblem) - - return res if res != float('INF') else -1 - - return dp(amount) +```java +class Solution { + public int coinChange(int[] coins, int amount) { + // 题目要求的最终结果是 dp(amount) + return dp(coins, amount); + } + + // 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币 + int dp(int[] coins, int amount) { + // base case + if (amount == 0) return 0; + if (amount < 0) return -1; + + int res = Integer.MAX_VALUE; + for (int coin : coins) { + // 计算子问题的结果 + int subProblem = dp(coins, amount - coin); + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); + } + + return res == Integer.MAX_VALUE ? -1 : res; + } +} ``` +> [!NOTE] +> 这里 `coinChange` 和 `dp` 函数的签名完全一样,所以理论上不需要额外写一个 `dp` 函数。但为了后文讲解方便,这里还是另写一个 `dp` 函数来实现主要逻辑。 +> +> 另外,我经常看到有读者留言问,子问题的结果为什么要加 1(`subProblem + 1`),而不是加硬币金额之类的。我这里统一提示一下,动态规划问题的关键是 `dp` 函数/数组的定义,你这个函数的返回值代表什么?你回过头去搞清楚这一点,然后就知道为什么要给子问题的返回值加 1 了。 + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ 至此,状态转移方程其实已经完成了,以上算法已经是暴力解法了,以上代码的数学形式就是状态转移方程: -![](../pictures/动态规划详解进阶/coin.png) +![](https://labuladong.online/algo/images/dynamic-programming/coin.png) 至此,这个问题其实就解决了,只不过需要消除一下重叠子问题,比如 `amount = 11, coins = {1,2,5}` 时画出递归树看看: -![](../pictures/动态规划详解进阶/5.jpg) +![](https://labuladong.online/algo/images/dynamic-programming/5.jpg) -**时间复杂度分析:子问题总数 x 每个子问题的时间**。 +**递归算法的时间复杂度分析:子问题总数 x 解决每个子问题所需的时间**。 -子问题总数为递归树节点个数,这个比较难看出来,是 O(n^k),总之是指数级别的。每个子问题中含有一个 for 循环,复杂度为 O(k)。所以总时间复杂度为 O(k * n^k),指数级别。 +子问题总数为递归树的节点个数,但算法会进行剪枝,剪枝的时机和题目给定的具体硬币面额有关,所以可以想象,这棵树生长的并不规则,确切算出树上有多少节点是比较困难的。对于这种情况,我们一般的做法是按照最坏的情况估算一个时间复杂度的上界。 -**2、带备忘录的递归** +假设目标金额为 `n`,给定的硬币个数为 `k`,那么递归树最坏情况下高度为 `n`(全用面额为 1 的硬币),然后再假设这是一棵满 `k` 叉树,则节点的总数在 `k^n` 这个数量级。 -只需要稍加修改,就可以通过备忘录消除子问题: +接下来看每个子问题的复杂度,由于每次递归包含一个 for 循环,复杂度为 $O(k)$,相乘得到总时间复杂度为 $O(k^n)$,指数级别。 -```python -def coinChange(coins: List[int], amount: int): - # 备忘录 - memo = dict() - def dp(n): - # 查备忘录,避免重复计算 - if n in memo: return memo[n] - - if n == 0: return 0 - if n < 0: return -1 - res = float('INF') - for coin in coins: - subproblem = dp(n - coin) - if subproblem == -1: continue - res = min(res, 1 + subproblem) - - # 记入备忘录 - memo[n] = res if res != float('INF') else -1 - return memo[n] - - return dp(amount) -``` +### 带备忘录的递归 -不画图了,很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 n,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。 +类似之前斐波那契数列的例子,只需要稍加修改,就可以通过备忘录消除子问题: -**3、dp 数组的迭代解法** +```java +class Solution { + int[] memo; -当然,我们也可以自底向上使用 dp table 来消除重叠子问题,`dp` 数组的定义和刚才 `dp` 函数类似,定义也是一样的: + public int coinChange(int[] coins, int amount) { + memo = new int[amount + 1]; + // 备忘录初始化为一个不会被取到的特殊值,代表还未被计算 + Arrays.fill(memo, -666); -**`dp[i] = x` 表示,当目标金额为 `i` 时,至少需要 `x` 枚硬币**。 + return dp(coins, amount); + } -```cpp -int coinChange(vector& coins, int amount) { - // 数组大小为 amount + 1,初始值也为 amount + 1 - vector dp(amount + 1, amount + 1); - // base case - dp[0] = 0; - for (int i = 0; i < dp.size(); i++) { - // 内层 for 在求所有子问题 + 1 的最小值 + int dp(int[] coins, int amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; + + int res = Integer.MAX_VALUE; for (int coin : coins) { - // 子问题无解,跳过 - if (i - coin < 0) continue; - dp[i] = min(dp[i], 1 + dp[i - coin]); + // 计算子问题的结果 + int subProblem = dp(coins, amount - coin); + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); } + // 把计算结果存入备忘录 + memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; + return memo[amount]; } - return (dp[amount] == amount + 1) ? -1 : dp[amount]; } ``` -![](../pictures/动态规划详解进阶/6.jpg) -PS:为啥 `dp` 数组初始化为 `amount + 1` 呢,因为凑成 `amount` 金额的硬币数最多只可能等于 `amount`(全用 1 元面值的硬币),所以初始化为 `amount + 1` 就相当于初始化为正无穷,便于后续取最小值。 +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
-### 三、最后总结 -第一个斐波那契数列的问题,解释了如何通过「备忘录」或者「dp table」的方法来优化递归树,并且明确了这两种方法本质上是一样的,只是自顶向下和自底向上的不同而已。 -第二个凑零钱的问题,展示了如何流程化确定「状态转移方程」,只要通过状态转移方程写出暴力递归解,剩下的也就是优化递归树,消除重叠子问题而已。 +不画图了,很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 `n`,即子问题数目为 $O(n)$。处理一个子问题的时间不变,仍是 $O(k)$,所以总的时间复杂度是 $O(kn)$。 -如果你不太了解动态规划,还能看到这里,真得给你鼓掌,相信你已经掌握了这个算法的设计技巧。 +### dp 数组的迭代解法 -**计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举**,穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。 +当然,我们也可以自底向上使用 dp table 来消除重叠子问题,关于「状态」「选择」和 base case 与之前没有区别,`dp` 数组的定义和刚才 `dp` 函数类似,也是把「状态」,也就是目标金额作为变量。不过 `dp` 函数体现在函数参数,而 `dp` 数组体现在数组索引: -列出动态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。 +**`dp` 数组的定义:当目标金额为 `i` 时,至少需要 `dp[i]` 枚硬币凑出**。 -备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门,除此之外,试问,还能玩出啥花活? +根据我们文章开头给出的动态规划代码框架可以写出如下解法: -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: +```java +class Solution { + public int coinChange(int[] coins, int amount) { + int[] dp = new int[amount + 1]; + // 数组大小为 amount + 1,初始值也为 amount + 1 + Arrays.fill(dp, amount + 1); + + // base case + dp[0] = 0; + // 外层 for 循环在遍历所有状态的所有取值 + for (int i = 0; i < dp.length; i++) { + // 内层 for 循环在求所有选择的最小值 + for (int coin : coins) { + // 子问题无解,跳过 + if (i - coin < 0) { + continue; + } + dp[i] = Math.min(dp[i], 1 + dp[i - coin]); + } + } + return (dp[amount] == amount + 1) ? -1 : dp[amount]; + } +} +``` -![labuladong](../pictures/labuladong.png) +> [!NOTE] +> 为啥 `dp` 数组中的值都初始化为 `amount + 1` 呢,因为凑成 `amount` 金额的硬币数最多只可能等于 `amount`(全用 1 元面值的硬币),所以初始化为 `amount + 1` 就相当于初始化为正无穷,便于后续取最小值。为啥不直接初始化为 int 型的最大值 `Integer.MAX_VALUE` 呢?因为后面有 `dp[i - coin] + 1`,这就会导致整型溢出。 -[上一篇:学习数据结构和算法读什么书](../算法思维系列/为什么推荐算法4.md) +![](https://labuladong.online/algo/images/dynamic-programming/6.jpg) -[下一篇:动态规划答疑篇](../动态规划系列/最优子结构.md) +## 三、最后总结 + +第一个斐波那契数列的问题,解释了如何通过「备忘录」或者「dp table」的方法来优化递归树,并且明确了这两种方法本质上是一样的,只是自顶向下和自底向上的不同而已。 + +第二个凑零钱的问题,展示了如何流程化确定「状态转移方程」,只要通过状态转移方程写出暴力递归解,剩下的也就是优化递归树,消除重叠子问题而已。 + +如果你不太了解动态规划,还能看到这里,真得给你鼓掌,相信你已经掌握了这个算法的设计技巧。 + +**计算机解决问题其实没有任何特殊的技巧,它唯一的解决办法就是穷举**,穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。 + +列出状态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。 + +备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门,除此之外,试问,还能玩出啥花活? -[目录](../README.md#目录) \ No newline at end of file +之后我们会有一章专门讲解动态规划问题,如果有任何问题都可以随时回来重读本文,希望读者在阅读每个题目和解法时,多往「状态」和「选择」上靠,才能对这套框架产生自己的理解,运用自如。 + + + + + + + +
+
+引用本文的文章 + + - [base case 和备忘录的初始值怎么定?](https://labuladong.online/algo/dynamic-programming/memo-fundamental/) + - [【强化练习】BFS 经典习题 II](https://labuladong.online/algo/problem-set/bfs-ii/) + - [【强化练习】二分搜索算法经典习题](https://labuladong.online/algo/problem-set/binary-search/) + - [【强化练习】单调队列的通用实现及经典习题](https://labuladong.online/algo/problem-set/monotonic-queue/) + - [【强化练习】同时运用两种思维解题](https://labuladong.online/algo/problem-set/binary-tree-combine-two-view/) + - [【强化练习】数学技巧相关习题](https://labuladong.online/algo/problem-set/math-tricks/) + - [一个方法团灭 LeetCode 打家劫舍问题](https://labuladong.online/algo/dynamic-programming/house-robber/) + - [一个方法团灭 LeetCode 股票买卖问题](https://labuladong.online/algo/dynamic-programming/stock-problem-summary/) + - [二叉树基础及常见类型](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) + - [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + - [分治算法解题套路框架](https://labuladong.online/algo/essential-technique/divide-and-conquer/) + - [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) + - [动态规划之最小路径和](https://labuladong.online/algo/dynamic-programming/minimum-path-sum/) + - [动态规划和回溯算法的思维转换](https://labuladong.online/algo/dynamic-programming/word-break/) + - [动态规划帮我通关了《辐射4》](https://labuladong.online/algo/dynamic-programming/freedom-trail/) + - [动态规划帮我通关了《魔塔》](https://labuladong.online/algo/dynamic-programming/magic-tower/) + - [动态规划穷举的两种视角](https://labuladong.online/algo/dynamic-programming/two-views-of-dp/) + - [动态规划设计:最大子数组](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) + - [动态规划设计:最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [对动态规划进行降维打击](https://labuladong.online/algo/dynamic-programming/space-optimization/) + - [拓展:归并排序详解及应用](https://labuladong.online/algo/practice-in-action/merge-sort/) + - [旅游省钱大法:加权最短路径](https://labuladong.online/algo/dynamic-programming/cheap-travel/) + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + - [算法学习和心流体验](https://labuladong.online/algo/fname.html?fname=心流) + - [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + - [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + - [经典动态规划:0-1 背包问题](https://labuladong.online/algo/dynamic-programming/knapsack1/) + - [经典动态规划:博弈问题](https://labuladong.online/algo/dynamic-programming/game-theory/) + - [经典动态规划:子集背包问题](https://labuladong.online/algo/dynamic-programming/knapsack2/) + - [经典动态规划:完全背包问题](https://labuladong.online/algo/dynamic-programming/knapsack3/) + - [经典动态规划:戳气球](https://labuladong.online/algo/dynamic-programming/burst-balloons/) + - [经典动态规划:最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/) + - [经典动态规划:正则表达式](https://labuladong.online/algo/dynamic-programming/regular-expression-matching/) + - [经典动态规划:编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/) + - [经典动态规划:高楼扔鸡蛋](https://labuladong.online/algo/dynamic-programming/egg-drop/) + - [老司机加油算法](https://labuladong.online/algo/frequency-interview/gas-station-greedy/) + - [背包问题的变体:目标和](https://labuladong.online/algo/dynamic-programming/target-sum/) + - [贪心算法解题套路框架](https://labuladong.online/algo/essential-technique/greedy/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [111. Minimum Depth of Binary Tree](https://leetcode.com/problems/minimum-depth-of-binary-tree/?show=1) | [111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/?show=1) | 🟢 | +| [112. Path Sum](https://leetcode.com/problems/path-sum/?show=1) | [112. 路径总和](https://leetcode.cn/problems/path-sum/?show=1) | 🟢 | +| [115. Distinct Subsequences](https://leetcode.com/problems/distinct-subsequences/?show=1) | [115. 不同的子序列](https://leetcode.cn/problems/distinct-subsequences/?show=1) | 🔴 | +| [139. Word Break](https://leetcode.com/problems/word-break/?show=1) | [139. 单词拆分](https://leetcode.cn/problems/word-break/?show=1) | 🟠 | +| [1696. Jump Game VI](https://leetcode.com/problems/jump-game-vi/?show=1) | [1696. 跳跃游戏 VI](https://leetcode.cn/problems/jump-game-vi/?show=1) | 🟠 | +| [221. Maximal Square](https://leetcode.com/problems/maximal-square/?show=1) | [221. 最大正方形](https://leetcode.cn/problems/maximal-square/?show=1) | 🟠 | +| [240. Search a 2D Matrix II](https://leetcode.com/problems/search-a-2d-matrix-ii/?show=1) | [240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/?show=1) | 🟠 | +| [256. Paint House](https://leetcode.com/problems/paint-house/?show=1)🔒 | [256. 粉刷房子](https://leetcode.cn/problems/paint-house/?show=1)🔒 | 🟠 | +| [279. Perfect Squares](https://leetcode.com/problems/perfect-squares/?show=1) | [279. 完全平方数](https://leetcode.cn/problems/perfect-squares/?show=1) | 🟠 | +| [343. Integer Break](https://leetcode.com/problems/integer-break/?show=1) | [343. 整数拆分](https://leetcode.cn/problems/integer-break/?show=1) | 🟠 | +| [365. Water and Jug Problem](https://leetcode.com/problems/water-and-jug-problem/?show=1) | [365. 水壶问题](https://leetcode.cn/problems/water-and-jug-problem/?show=1) | 🟠 | +| [542. 01 Matrix](https://leetcode.com/problems/01-matrix/?show=1) | [542. 01 矩阵](https://leetcode.cn/problems/01-matrix/?show=1) | 🟠 | +| [576. Out of Boundary Paths](https://leetcode.com/problems/out-of-boundary-paths/?show=1) | [576. 出界的路径数](https://leetcode.cn/problems/out-of-boundary-paths/?show=1) | 🟠 | +| [62. Unique Paths](https://leetcode.com/problems/unique-paths/?show=1) | [62. 不同路径](https://leetcode.cn/problems/unique-paths/?show=1) | 🟠 | +| [63. Unique Paths II](https://leetcode.com/problems/unique-paths-ii/?show=1) | [63. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/?show=1) | 🟠 | +| [70. Climbing Stairs](https://leetcode.com/problems/climbing-stairs/?show=1) | [70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/?show=1) | 🟢 | +| [91. Decode Ways](https://leetcode.com/problems/decode-ways/?show=1) | [91. 解码方法](https://leetcode.cn/problems/decode-ways/?show=1) | 🟠 | +| - | [剑指 Offer 04. 二维数组中的查找](https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 10- I. 斐波那契数列](https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 10- II. 青蛙跳台阶问题](https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 14- I. 剪绳子](https://leetcode.cn/problems/jian-sheng-zi-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 46. 把数字翻译成字符串](https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/?show=1) | 🟠 | +| - | [剑指 Offer II 091. 粉刷房子](https://leetcode.cn/problems/JEj789/?show=1) | 🟠 | +| - | [剑指 Offer II 097. 子序列的数目](https://leetcode.cn/problems/21dk04/?show=1) | 🔴 | +| - | [剑指 Offer II 098. 路径的数目](https://leetcode.cn/problems/2AoeFn/?show=1) | 🟠 | +| - | [剑指 Offer II 103. 最少的硬币数目](https://leetcode.cn/problems/gaM7Ch/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\215\225\350\257\215\346\213\274\346\216\245.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\215\225\350\257\215\346\213\274\346\216\245.md" new file mode 100644 index 0000000000..cba8ca949b --- /dev/null +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\215\225\350\257\215\346\213\274\346\216\245.md" @@ -0,0 +1,571 @@ +# 动态规划和回溯算法的思维转换 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [139. Word Break](https://leetcode.com/problems/word-break/) | [139. 单词拆分](https://leetcode.cn/problems/word-break/) | 🟠 | +| [140. Word Break II](https://leetcode.com/problems/word-break-ii/) | [140. 单词拆分 II](https://leetcode.cn/problems/word-break-ii/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树系列算法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +之前 [手把手带你刷二叉树(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 把递归穷举划分为「遍历」和「分解问题」两种思路,其中「遍历」的思路扩展延伸一下就是 [回溯算法](https://labuladong.online/algo/essential-technique/backtrack-framework/),「分解问题」的思路可以扩展成 [动态规划算法](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/)。 + +这种思维转换不止局限于二叉树相关的算法,本文就跳出二叉树类型问题,来看看实际算法题中如何把问题抽象成树形结构,见招拆招逐步优化,从而进行「遍历」和「分解问题」的思维转换,从回溯算法顺滑地切换到动态规划算法。 + +先说句题外话,前文 [动态规划核心框架详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 说,**标准的动态规划问题一定是求最值的**,因为动态规划类型问题有一个性质叫做「最优子结构」,即从子问题的最优解推导出原问题的最优解。 + +但在我们平常的语境中,就算不是求最值的题目,只要看见使用备忘录消除重叠子问题,我们一般都称它为动态规划算法。严格来讲这是不符合动态规划问题的定义的,说这种解法叫做「带备忘录的 DFS 算法」可能更准确些。不过咱也不用太纠结这种名词层面的细节,既然大家叫的顺口,就叫它动态规划也无妨。 + +本文讲解的两道题目也不是求最值的,但依然会把他们的解法称为动态规划解法,这里提前跟大家说下这个变通,免得严谨的读者疑惑。其他不多说了,直接看题目吧。 + + + + + +## 单词拆分 I + + + +首先看下力扣第 139 题「单词拆分」: + + + +函数签名如下: + +```java +boolean wordBreak(String s, List wordDict); +``` + +这是一道非常高频的面试题,我们来思考下如何通过「遍历」和「分解问题」的思路来解决它。 + +### 遍历的思路(回溯解法) + +**先说说「遍历」的思路,也就是用回溯算法解决本题**。回溯算法最经典的应用就是排列组合相关的问题了,不难发现这道题换个说法也可以变成一个排列问题: + +现在给你一个不包含重复单词的单词列表 `wordDict` 和一个字符串 `s`,请你判断是否可以从 `wordDict` 中选出若干单词的排列(可以重复挑选)构成字符串 `s`。 + +这就是前文 [回溯算法秒杀排列组合问题的九种变体](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) 中讲到的最后一种变体:元素无重可复选的排列问题,前文我写了一个 `permuteRepeat` 函数,代码如下: + +```java +class Solution { + List> res = new LinkedList<>(); + LinkedList track = new LinkedList<>(); + + // 元素无重可复选的全排列 + public List> permuteRepeat(int[] nums) { + backtrack(nums); + return res; + } + + // 回溯算法核心函数 + void backtrack(int[] nums) { + // base case,到达叶子节点 + if (track.size() == nums.length) { + // 收集根到叶子节点路径上的值 + res.add(new LinkedList(track)); + return; + } + + // 回溯算法标准框架 + for (int i = 0; i < nums.length; i++) { + // 做选择 + track.add(nums[i]); + // 进入下一层回溯树 + backtrack(nums); + // 取消选择 + track.removeLast(); + } + } +} +``` + +给这个函数输入 `nums = [1,2,3]`,输出是 3^3 = 27 种可能的组合: + + + + + +```java +[ + [1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3], + [2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3], + [3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3] +] +``` + + + +这段代码实际上就是遍历一棵高度为 `N + 1` 的满 `N` 叉树(`N` 为 `nums` 的长度),其中根到叶子的每条路径上的元素就是一个排列结果: + +![](https://labuladong.online/algo/images/word-break/1.jpeg) + +类比一下,本文讲的这道题也有异曲同工之妙,假设 `wordDict = ["a", "aa", "ab"], s = "aaab"`,想用 `wordDict` 中的单词拼出 `s`,其实也面对着类似的一棵 `M` 叉树,`M` 为 `wordDict` 中单词的个数,**你需要做的就是站在回溯树的每个节点上,看看哪个单词能够匹配 `s[i..]` 的前缀,从而判断应该往哪条树枝上走**: + +![](https://labuladong.online/algo/images/word-break/2.jpeg) + +然后,按照前文 [回溯算法框架详解](https://labuladong.online/algo/essential-technique/backtrack-framework/) 所说,你把 `backtrack` 函数理解成在回溯树上游走的一个指针,维护每个节点上的变量 `i`,即可遍历整棵回溯树,寻找出匹配 `s` 的组合。 + +回溯算法解法代码如下: + +```java +class Solution { + List wordDict; + // 记录是否找到一个合法的答案 + boolean found = false; + // 记录回溯算法的路径 + LinkedList track = new LinkedList<>(); + + // 主函数 + public boolean wordBreak(String s, List wordDict) { + this.wordDict = wordDict; + // 执行回溯算法穷举所有可能的组合 + backtrack(s, 0); + return found; + } + + // 回溯算法框架 + void backtrack(String s, int i) { + // base case + if (found) { + // 如果已经找到答案,就不要再递归搜索了 + return; + } + if (i == s.length()) { + // 整个 s 都被匹配完成,找到一个合法答案 + found = true; + return; + } + + // 回溯算法框架 + for (String word : wordDict) { + // 看看哪个单词能够匹配 s[i..] 的前缀 + int len = word.length(); + if (i + len <= s.length() + && s.substring(i, i + len).equals(word)) { + // 找到一个单词匹配 s[i..i+len) + // 做选择 + track.addLast(word); + // 进入回溯树的下一层,继续匹配 s[i+len..] + backtrack(s, i + len); + // 撤销选择 + track.removeLast(); + } + } + } +} +``` + +这段代码就是严格按照回溯算法框架写出来的,应该不难理解,但这段代码无法通过所有测试用例,我们按照 [算法时空复杂度使用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) 中讲到的方法来分析一下它的时间复杂度。 + +递归函数的时间复杂度的粗略估算方法就是用递归函数调用次数(递归树的节点数) x 递归函数本身的复杂度。对于这道题来说,递归树的每个节点其实就是对 `s` 进行的一次切割,那么最坏情况下 `s` 能有多少种切割呢?长度为 `N` 的字符串 `s` 中共有 `N - 1` 个「缝隙」可供切割,每个缝隙可以选择「切」或者「不切」,所以 `s` 最多有 $O(2^N)$ 种切割方式,即递归树上最多有 $O(2^N)$ 个节点。 + +当然,实际情况可定会好一些,毕竟存在剪枝逻辑,但从最坏复杂度的角度来看,递归树的节点个数确实是指数级别的。 + +那么 `backtrack` 函数本身的时间复杂度是多少呢?主要的时间消耗是遍历 `wordDict` 寻找匹配 `s[i..]` 的前缀的单词: + +```java +// 遍历 wordDict 的所有单词 +for (String word : wordDict) { + // 看看哪个单词能够匹配 s[i..] 的前缀 + int len = word.length(); + if (i + len <= s.length() + && s.substring(i, i + len).equals(word)) { + // 找到一个单词匹配 s[i..i+len) + // ... + } +} +``` + +设 `wordDict` 的长度为 `M`,字符串 `s` 的长度为 `N`,那么这段代码的最坏时间复杂度是 $O(MN)$(for 循环 $O(M)$,Java 的 `substring` 方法 $O(N)$),所以总的时间复杂度是 $O(2^N * MN)$。 + +这里顺便说一个细节优化,其实你也可以反过来,通过穷举 `s[i..]` 的前缀去判断 `wordDict` 中是否有对应的单词: + +```java +// 注意,要转化成哈希集合,提高 contains 方法的效率 +HashSet wordDict = new HashSet<>(wordDict); + +// 遍历 s[i..] 的所有前缀 +for (int len = 1; i + len <= s.length(); len++) { + // 看看 wordDict 中是否有单词能匹配 s[i..] 的前缀 + String prefix = s.substring(i, i + len); + if (wordDict.contains(prefix)) { + // 找到一个单词匹配 s[i..i+len) + // ... + } +} +``` + +这段代码和刚才那段代码的结果是一样的,但这段代码的时间复杂度变成了 $O(N^2)$,和刚才的代码不同。 + +到底哪样子好呢?这要看题目给的数据范围。本题说了 `1 <= s.length <= 300, 1 <= wordDict.length <= 1000`,所以 $O(N^2)$ 的结果较小,这段代码的实际运行效率应该稍微高一些,这个是一个细节的优化,你可以自己做一下,我就不写了。 + +不过即便你优化这段代码,总的时间复杂度依然是指数级的 $O(2^N * N^2)$,是无法通过所有测试用例的,那么问题出在哪里呢? + +比如输入 `wordDict = ["a", "aa", "b"], s = "aaab"`,你注意回溯算法穷举时会存在重复的情况: + +![](https://labuladong.online/algo/images/word-break/3.jpeg) + +图中标红的这两部分,虽然经历了不同的切分,但是切分得出的结果是相同的 `"aab"`,所以这两个节点下面的子树也是重复的,即存在冗余计算,这也是这个算法复杂度为指数级别的原因。 + +### 利用后序位置优化 + +不管是什么算法,消除冗余计算的方法就是加备忘录。回溯算法也可以加备忘录,我们可以称之为「剪枝」,即把冗余的子树给它剪掉。 + +就比如面对这个 `"aab"` 子串的局面,我希望让备忘录告诉我,这个 `"aab"` 到底能不能被成功切分?如果之前尝试过不能切分的话,我就直接跳过,不用遍历子树去穷举切分了,从而优化效率。如果之前尝试过能成功切分的话,那也不关备忘录什么事情了,因为 `found == true` 本身就是个 base case,整个递归都会被终止。 + +正如 [二叉树/递归系列算法通用心法](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 对前序/后序位置的分析,要想让备忘录做到这一点,需要在后序位置上更新备忘录,因为这个 `"aab"` 其实是一棵子树,对吧?**你需要在遍历完子树的时候,在备忘录中记录下该子树是否可以被成功切分**。 + +回溯函数 `backtrack` 为遍历而生,本身没有返回值,即没有从子树传递回来的信息。但针对这道题,我们还是有办法的,因为这不是有个外部变量 `found` 吗?这个变量就可以告诉我们子树是否能够成功切分: + +**如果 `found` 为 false,说明还没找到一个成功的切分,也就间接说明当前子树不能成功切分**。此时我们可以在备忘录里面记一笔,从而消除掉冗余的穷举。 + +具体到代码上,只需稍加修改,即可实现备忘录功能,为了节约篇幅,我只给出修改的部分,: + +```java +class Solution { + // 备忘录,存储不能切分的子串(子树),从而避免重复计算 + HashSet memo = new HashSet<>(); + + // ... + + void backtrack(String s, int i) { + if (found) { + return; + } + if (i == s.length()) { + found = true; + return; + } + + // 新增的剪枝逻辑,查询子串(子树)是否已经计算过 + String suffix = s.substring(i); + if (memo.contains(suffix)) { + // 当前子串(子树)不能被切分,就不用继续递归了 + return; + } + + for (String word : wordDict) { + // ... + } + + // 后序位置,将不能切分的子串(子树)记录到备忘录 + if (!found) { + memo.add(suffix); + } + } +} +``` + +### 分解问题的思路(动态规划) + +上面能用回溯算法解决这个问题,其实还是这道题比较简单,我们可以借助 `found` 变量在后序位置更新备忘录,你会看到后面讲的单词拆分 II 就不能这么搞了。 + +要在后序位置更新备忘录存储子树的答案,一般还是要借助递归函数的返回值,因此还是要用「分解问题」的思维模式。 + +我们刚才以排列组合的视角思考这个问题,现在我们换一种视角,思考一下是否能够把原问题分解成规模更小,结构相同的子问题,然后通过子问题的结果计算原问题的结果。 + +对于输入的字符串 `s`,如果我能够从单词列表 `wordDict` 中找到一个单词匹配 `s` 的前缀 `s[0..k]`,那么只要我能拼出 `s[k+1..]`,就一定能拼出整个 `s`。换句话说,我把规模较大的原问题 `wordBreak(s[0..])` 分解成了规模较小的子问题 `wordBreak(s[k+1..])`,然后通过子问题的解反推出原问题的解。 + +有了这个思路就可以定义一个 `dp` 函数,并给出该函数的定义: + +```java +// 定义:返回 s[i..] 是否能够被拼出 +int dp(String s, int i); + +// 计算整个 s 是否能被拼出,调用 dp(s, 0) +``` + +有了这个函数定义,就可以把刚才的逻辑大致翻译成伪码: + +```java +List wordDict; + +// 定义:返回 s[i..] 是否能够被拼出 +int dp(String s, int i) { + // base case,s[i..] 是空串 + if (i == s.length()) { + return true; + } + // 遍历 wordDict,看看哪些单词是 s[i..] 的前缀 + for (Strnig word : wordDict) { + // word 是 s[i..] 的前缀 + if (s.substring(i).startsWith(word)) { + int len = word.length(); + // 只要 s[i+len..] 可以被拼出,s[i..] 就能被拼出 + if (dp(s, i + len) == true) { + return true; + } + } + } + // 所有单词都尝试过,无法拼出整个 s + return false; +} +``` + +类似之前讲的回溯算法,`dp` 函数中的 for 循环也可以优化一下: + +```java +// 注意,用哈希集合快速判断元素是否存在 +HashSet wordDict; + +// 定义:返回 s[i..] 是否能够被拼出 +int dp(String s, int i) { + // base case,s[i..] 是空串 + if (i == s.length()) { + return true; + } + + // 遍历 s[i..] 的所有前缀,看看哪些前缀存在 wordDict 中 + for (int len = 1; i + len <= s.length(); len++) { + // wordDict 中存在 s[i..len) + if (wordDict.contains(s.substring(i, i + len))) { + // 只要 s[i+len..] 可以被拼出,s[i..] 就能被拼出 + if (dp(s, i + len) == true) { + return true; + } + } + } + // 所有单词都尝试过,无法拼出整个 s + return false; +} +``` + +对于这个 `dp` 函数,指针 `i` 的位置就是「状态」,所以我们可以通过添加备忘录的方式优化效率,避免对相同的子问题进行冗余计算。最终的解法代码如下: + +```java +class Solution { + // 用哈希集合方便快速判断是否存在 + HashSet wordDict; + // 备忘录,-1 代表未计算,0 代表无法凑出,1 代表可以凑出 + int[] memo; + + // 主函数 + public boolean wordBreak(String s, List wordDict) { + // 转化为哈希集合,快速判断元素是否存在 + this.wordDict = new HashSet<>(wordDict); + // 备忘录初始化为 -1 + this.memo = new int[s.length()]; + Arrays.fill(memo, -1); + return dp(s, 0); + } + + // 定义:s[i..] 是否能够被拼出 + boolean dp(String s, int i) { + // base case + if (i == s.length()) { + return true; + } + // 防止冗余计算 + if (memo[i] != -1) { + return memo[i] == 0 ? false : true; + } + + // 遍历 s[i..] 的所有前缀 + for (int len = 1; i + len <= s.length(); len++) { + // 看看哪些前缀存在 wordDict 中 + String prefix = s.substring(i, i + len); + if (wordDict.contains(prefix)) { + // 找到一个单词匹配 s[i..i+len) + // 只要 s[i+len..] 可以被拼出,s[i..] 就能被拼出 + boolean subProblem = dp(s, i + len); + if (subProblem == true) { + memo[i] = 1; + return true; + } + } + } + // s[i..] 无法被拼出 + memo[i] = 0; + return false; + } +} +``` + +> [!TIP] +> 注意到计算 `prefix` 的过程中,我们是直接调用编程语言提供的子串截取函数,这个函数的时间复杂度是 $O(N)$。不难发现截取子串的开始索引是固定的 `i`,结束索引是递增的 `j`,所以我们手动维护这个 `prefix` 子串,避免调用子串截取函数,进一步提高效率。这个小优化就留给你来做吧。 + + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ +这个解法能够通过所有测试用例,我们根据 [算法时空复杂度使用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) 来算一下它的时间复杂度: + +因为有备忘录的辅助,消除了递归树上的重复节点,使得递归函数的调用次数从指数级别降低为状态的个数 $O(N)$,函数本身的复杂度还是 $O(N^2)$,所以总的时间复杂度是 $O(N^3)$,相较回溯算法的效率有大幅提升。 + +## 单词拆分 II + +有了上一道题的铺垫,力扣第 140 题「单词拆分 II」就容易多了,先看下题目: + + + +相较上一题,这道题不是单单问你 `s` 是否能被拼出,还要问你是怎么拼的,其实只要把之前的解法稍微改一改就可以解决这道题。 + +### 遍历的思路(回溯算法) + +上一道题的回溯算法维护一个 `found` 变量,只要找到一种拼接方案就提前结束遍历回溯树,那么在这道题中我们不要提前结束遍历,并把所有可行的拼接方案收集起来就能得到答案: + +```java +class Solution { + // 记录结果 + List res = new LinkedList<>(); + // 记录回溯算法的路径 + LinkedList track = new LinkedList<>(); + List wordDict; + + // 主函数 + public List wordBreak(String s, List wordDict) { + this.wordDict = wordDict; + // 执行回溯算法穷举所有可能的组合 + backtrack(s, 0); + return res; + } + + // 回溯算法框架 + void backtrack(String s, int i) { + // base case + if (i == s.length()) { + // 找到一个合法组合拼出整个 s,转化成字符串 + res.add(String.join(" ", track)); + return; + } + + // 回溯算法框架 + for (String word : wordDict) { + // 看看哪个单词能够匹配 s[i..] 的前缀 + int len = word.length(); + if (i + len <= s.length() + && s.substring(i, i + len).equals(word)) { + // 找到一个单词匹配 s[i..i+len) + // 做选择 + track.addLast(word); + // 进入回溯树的下一层,继续匹配 s[i+len..] + backtrack(s, i + len); + // 撤销选择 + track.removeLast(); + } + } + } +} +``` + +这个解法的时间复杂度和前一道题类似,依然是 $O(2^N * MN)$,但由于这道题给的数据规模较小,所以可以通过所有测试用例。 + +### 是否可以利用后序位置优化? + +和之前类似,这个解法还是有优化空间的,依然是这种情况: + +![](https://labuladong.online/algo/images/word-break/3.jpeg) + +对于重复的子树,依然会造成没有必要的重复遍历,我们依然可以通过备忘录的方式进行优化,即可以在备忘录缓存子串 `"aab"` 的切分结果,避免重复遍历相同的子树。 + +但用回溯算法就不好加备忘录了,因为回溯算法的 `track` 变量仅维护了从根节点到当前节点走过的路径,并没有记录子树的信息。 + +所以,这种题目想要消除重叠子问题的话一般要用分解问题的思路,利用函数返回值来更新备忘录。 + +### 分级问题的思路(动态规划) + +这个问题也可以用分解问题的思维解决,把上一道题的 `dp` 函数稍作修改即可: + +```java +class Solution { + HashSet wordDict; + // 备忘录 + List[] memo; + + public List wordBreak(String s, List wordDict) { + this.wordDict = new HashSet<>(wordDict); + memo = new List[s.length()]; + return dp(s, 0); + } + + // 定义:返回用 wordDict 构成 s[i..] 的所有可能 + List dp(String s, int i) { + List res = new LinkedList<>(); + if (i == s.length()) { + res.add(""); + return res; + } + // 防止冗余计算 + if (memo[i] != null) { + return memo[i]; + } + + // 遍历 s[i..] 的所有前缀 + for (int len = 1; i + len <= s.length(); len++) { + // 看看哪些前缀存在 wordDict 中 + String prefix = s.substring(i, i + len); + if (wordDict.contains(prefix)) { + // 找到一个单词匹配 s[i..i+len) + List subProblem = dp(s, i + len); + // 构成 s[i+len..] 的所有组合加上 prefix + // 就是构成构成 s[i] 的所有组合 + for (String sub : subProblem) { + if (sub.isEmpty()) { + // 防止多余的空格 + res.add(prefix); + } else { + res.add(prefix + " " + sub); + } + } + } + } + // 存入备忘录 + memo[i] = res; + + return res; + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ +这个解法依然用备忘录消除了重叠子问题,所以 `dp` 函数递归调用的次数减少为 $O(N)$,但 `dp` 函数本身的时间复杂度上升了,因为 `subProblem` 是一个子集列表,它的长度是指数级的。 + +再加上拼接字符串的效率并不高,且还要消耗备忘录去存储所有子问题的结果,所以从 Big O 的角度来分析,这个算法的时间复杂度并不比回溯算法低,依然是指数级别;但这个解法确实消除了重叠子问题,所以是要比回溯算法高明一些。 + +综上,我们处理排列组合问题时一般使用回溯算法去「遍历」回溯树,而不用「分解问题」的思路去处理,因为存储子问题的结果就需要大量的时间和空间,除非重叠子问题的数量较多的极端情况,否则得不偿失。 + +以上就是本文的全部内容,希望你能对回溯思路和分解问题的思路有更深刻的理解。 + + + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" index 6b5ec28bdb..ff4cd9c9be 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" @@ -1,47 +1,85 @@ -# 团灭 LeetCode 股票买卖问题 +# 一个方法团灭 LeetCode 股票买卖问题 -很多读者抱怨 LeetCode 的股票系列问题奇技淫巧太多,如果面试真的遇到这类问题,基本不会想到那些巧妙的办法,怎么办?**所以本文拒绝奇技淫巧,而是稳扎稳打,只用一种通用方法解决所用问题,以不变应万变**。 -这篇文章用状态机的技巧来解决,可以全部提交通过。不要觉得这个名词高大上,文学词汇而已,实际上就是 DP table,看一眼就明白了。 -PS:本文参考自[英文版 LeetCode 的一篇题解](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/discuss/39038)。 +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [121. Best Time to Buy and Sell Stock](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/) | [121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) | 🟢 | +| [122. Best Time to Buy and Sell Stock II](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/) | [122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) | 🟠 | +| [123. Best Time to Buy and Sell Stock III](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/) | [123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/) | 🔴 | +| [188. Best Time to Buy and Sell Stock IV](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/) | [188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) | 🔴 | +| [309. Best Time to Buy and Sell Stock with Cooldown](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/) | [309. 最佳买卖股票时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) | 🟠 | +| [714. Best Time to Buy and Sell Stock with Transaction Fee](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) | [714. 买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +很多读者抱怨力扣上的股票系列问题的解法太多,如果面试真的遇到这类问题,基本不会想到那些巧妙的办法,怎么办?**所以本文不讲那些过于巧妙的思路,而是稳扎稳打,只用一种通用方法解决所有问题,以不变应万变**。 + +这篇文章参考 [英文版高赞题解](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems) 的思路,用状态机的技巧来解决,可以全部提交通过。不要觉得这个名词高大上,文学词汇而已,实际上就是 DP table,看一眼就明白了。 先随便抽出一道题,看看别人的解法: -```cpp -int maxProfit(vector& prices) { + + + + +```java +int maxProfit(int[] prices) { if(prices.empty()) return 0; - int s1=-prices[0],s2=INT_MIN,s3=INT_MIN,s4=INT_MIN; - - for(int i=1;i + +第一题是只进行一次交易,相当于 `k = 1`;第二题是不限交易次数,相当于 `k = +infinity`(正无穷);第三题是只进行 2 次交易,相当于 `k = 2`;剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种,都很容易处理。 + +下面言归正传,开始解题。 + -![](../pictures/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98/title.png) -第一题是只进行一次交易,相当于 k = 1;第二题是不限交易次数,相当于 k = +infinity(正无穷);第三题是只进行 2 次交易,相当于 k = 2;剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种,都很容易处理。 -如果你还不熟悉题目,可以去 LeetCode 查看这些题目的内容,本文为了节省篇幅,就不列举这些题目的具体内容了。下面言归正传,开始解题。 -**一、穷举框架** -首先,还是一样的思路:如何穷举?这里的穷举思路和上篇文章递归的思想不太一样。 -递归其实是符合我们思考的逻辑的,一步步推进,遇到无法解决的就丢给递归,一不小心就做出来了,可读性还很好。缺点就是一旦出错,你也不容易找到错误出现的原因。比如上篇文章的递归解法,肯定还有计算冗余,但确实不容易找到。 +## 一、穷举框架 -而这里,我们不用递归思想进行穷举,而是利用「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。 +首先,还是一样的思路:如何穷举? + +[动态规划核心套路](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 说过,动态规划算法本质上就是穷举「状态」,然后在「选择」中选择最优解。 + +那么对于这道题,我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。 ```python for 状态1 in 状态1的所有取值: @@ -50,14 +88,27 @@ for 状态1 in 状态1的所有取值: dp[状态1][状态2][...] = 择优(选择1,选择2...) ``` -比如说这个问题,**每天都有三种「选择」**:买入、卖出、无操作,我们用 buy, sell, rest 表示这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作。 +比如说这个问题,**每天都有三种「选择」**:买入、卖出、无操作,我们用 `buy`, `sell`, `rest` 表示这三种选择。 + +但问题是,并不是每天都可以任意选择这三种选择的,因为 `sell` 必须在 `buy` 之后,`buy` 必须在 `sell` 之后。那么 `rest` 操作还应该分两种状态,一种是 `buy` 之后的 `rest`(持有了股票),一种是 `sell` 之后的 `rest`(没有持有股票)。而且别忘了,我们还有交易次数 `k` 的限制,就是说你 `buy` 还只能在 `k > 0` 的前提下操作。 + +> [!NOTE] +> 注意我在本文会频繁使用「交易」这个词,**我们把一次买入和一次卖出定义为一次「交易」**。 + + + -很复杂对吧,不要怕,我们现在的目的只是穷举,你有再多的状态,老夫要做的就是一把梭全部列举出来。**这个问题的「状态」有三个**,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合: + + + +很复杂对吧,不要怕,我们现在的目的只是穷举,你有再多的状态,老夫要做的就是一把梭全部列举出来。 + +**这个问题的「状态」有三个**,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 `rest` 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合: ```python dp[i][k][0 or 1] -0 <= i <= n-1, 1 <= k <= K -n 为天数,大 K 为最多交易数 +0 <= i <= n - 1, 1 <= k <= K +n 为天数,大 K 为交易数的上限,0 和 1 代表是否持有股票。 此问题共 n × K × 2 种状态,全部穷举就能搞定。 for 0 <= i < n: @@ -68,54 +119,90 @@ for 0 <= i < n: 而且我们可以用自然语言描述出每一个状态的含义,比如说 `dp[3][2][1]` 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易。再比如 `dp[2][3][0]` 的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 3 次交易。很容易理解,对吧? -我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K 次交易,最多获得多少利润。读者可能问为什么不是 dp[n - 1][K][1]?因为 [1] 代表手上还持有股票,[0] 表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。 +我们想求的最终答案是 `dp[n - 1][K][0]`,即最后一天,最多允许 `K` 次交易,最多获得多少利润。 + +读者可能问为什么不是 `dp[n - 1][K][1]`?因为 `dp[n - 1][K][1]` 代表到最后一天手上还持有股票,`dp[n - 1][K][0]` 表示最后一天手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。 记住如何解释「状态」,一旦你觉得哪里不好理解,把它翻译成自然语言就容易理解了。 -**二、状态转移框架** -现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。只看「持有状态」,可以画个状态转移图。 -![](../pictures/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98/1.png) + + + + +## 二、状态转移框架 + +现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。 + +只看「持有状态」,可以画个状态转移图: + +![](https://labuladong.online/algo/images/stock/1.png) 通过这个图可以很清楚地看到,每种状态(0 和 1)是如何转移而来的。根据这个图,我们来写一下状态转移方程: -``` +```python dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) - max( 选择 rest , 选择 sell ) + max( 今天选择 rest, 今天选择 sell ) +``` -解释:今天我没有持有股票,有两种可能: -要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有; -要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。 +解释:今天我没有持有股票,有两种可能,我从这两种可能中求最大利润: -dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) - max( 选择 rest , 选择 buy ) +1、我昨天就没有持有,且截至昨天最大交易次数限制为 `k`;然后我今天选择 `rest`,所以我今天还是没有持有,最大交易次数限制依然为 `k`。 + +2、我昨天持有股票,且截至昨天最大交易次数限制为 `k`;但是今天我 `sell` 了,所以我今天没有持有股票了,最大交易次数限制依然为 `k`。 -解释:今天我持有着股票,有两种可能: -要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票; -要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。 +```python +dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) + max( 今天选择 rest, 今天选择 buy ) ``` -这个解释应该很清楚了,如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。 +解释:今天我持有着股票,最大交易次数限制为 `k`,那么对于昨天来说,有两种可能,我从这两种可能中求最大利润: -现在,我们已经完成了动态规划中最困难的一步:状态转移方程。**如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了。**不过还差最后一点点,就是定义 base case,即最简单的情况。 -``` -dp[-1][k][0] = 0 -解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。 -dp[-1][k][1] = -infinity -解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。 -dp[i][0][0] = 0 -解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。 -dp[i][0][1] = -infinity -解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。 +1、我昨天就持有着股票,且截至昨天最大交易次数限制为 `k`;然后今天选择 `rest`,所以我今天还持有着股票,最大交易次数限制依然为 `k`。 + +2、我昨天本没有持有,且截至昨天最大交易次数限制为 `k - 1`;但今天我选择 `buy`,所以今天我就持有股票了,最大交易次数限制为 `k`。 + +> [!NOTE] +> 这里着重提醒一下,**时刻牢记「状态」的定义**,状态 `k` 的定义并不是「已进行的交易次数」,而是「最大交易次数的上限限制」。如果确定今天进行一次交易,且要保证截至今天最大交易次数上限为 `k`,那么昨天的最大交易次数上限必须是 `k - 1`。举个具体的例子,比方说要求你的银行卡里今天至少有 100 块钱,且你确定你今天可以赚 10 块钱,那么你就要保证昨天的银行卡要至少剩下 90 块钱。 + +这个解释应该很清楚了,如果 `buy`,就要从利润中减去 `prices[i]`,如果 `sell`,就要给利润增加 `prices[i]`。今天的最大利润就是这两种可能选择中较大的那个。 + +注意 `k` 的限制,在选择 `buy` 的时候相当于开启了一次交易,那么对于昨天来说,交易次数的上限 `k` 应该减小 1。 + +> [!NOTE] +> 这里补充修正一点,以前我以为在 `sell` 的时候给 `k` 减小 1 和在 `buy` 的时候给 `k` 减小 1 是等效的,但细心的读者向我提出质疑,经过深入思考我发现前者确实是错误的,因为交易是从 `buy` 开始,如果 `buy` 的选择不改变交易次数 `k` 的话,会出现交易次数超出限制的的错误。 + + + + + + + +现在,我们已经完成了动态规划中最困难的一步:状态转移方程。**如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了**。不过还差最后一点点,就是定义 base case,即最简单的情况。 + +```python +dp[-1][...][0] = 0 +解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0。 + +dp[-1][...][1] = -infinity +解释:还没开始的时候,是不可能持有股票的。 +因为我们的算法要求一个最大值,所以初始值设为一个最小值,方便取最大值。 + +dp[...][0][0] = 0 +解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0。 + +dp[...][0][1] = -infinity +解释:不允许交易的情况下,是不可能持有股票的。 +因为我们的算法要求一个最大值,所以初始值设为一个最小值,方便取最大值。 ``` 把上面的状态转移方程总结一下: -``` +```python base case: -dp[-1][k][0] = dp[i][0][0] = 0 -dp[-1][k][1] = dp[i][0][1] = -infinity +dp[-1][...][0] = dp[...][0][0] = 0 +dp[-1][...][1] = dp[...][0][1] = -infinity 状态转移方程: dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) @@ -124,13 +211,23 @@ dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) 读者可能会问,这个数组索引是 -1 怎么编程表示出来呢,负无穷怎么表示呢?这都是细节问题,有很多方法实现。现在完整的框架已经完成,下面开始具体化。 -**三、秒杀题目** -**第一题,k = 1** + + + + + +## 三、秒杀题目 + +### 121. 买卖股票的最佳时机 + +**第一题,先说力扣第 121 题「买卖股票的最佳时机」,相当于 `k = 1` 的情况**: + + 直接套状态转移方程,根据 base case,可以做一些化简: -``` +```python dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]) dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) = max(dp[i-1][1][1], -prices[i]) @@ -154,34 +251,58 @@ for (int i = 0; i < n; i++) { return dp[n - 1][0]; ``` -显然 i = 0 时 dp[i-1] 是不合法的。这是因为我们没有对 i 的 base case 进行处理。可以这样处理: +显然 `i = 0` 时 `i - 1` 是不合法的索引,这是因为我们没有对 `i` 的 base case 进行处理,可以这样给一个特化处理: + + + + ```java -for (int i = 0; i < n; i++) { - if (i - 1 == -1) { - dp[i][0] = 0; - // 解释: - // dp[i][0] - // = max(dp[-1][0], dp[-1][1] + prices[i]) - // = max(0, -infinity + prices[i]) = 0 - dp[i][1] = -prices[i]; - //解释: - // dp[i][1] - // = max(dp[-1][1], dp[-1][0] - prices[i]) - // = max(-infinity, 0 - prices[i]) - // = -prices[i] - continue; - } - dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); - dp[i][1] = Math.max(dp[i-1][1], -prices[i]); +if (i - 1 == -1) { + dp[i][0] = 0; + // 根据状态转移方程可得: + // dp[i][0] + // = max(dp[-1][0], dp[-1][1] + prices[i]) + // = max(0, -infinity + prices[i]) = 0 + // = max(dp[-1][0], dp[-1][1] + prices[i]) + // = max(0, -infinity + prices[i]) = 0 + + dp[i][1] = -prices[i]; + // 根据状态转移方程可得: + // dp[i][1] + // = max(dp[-1][1], dp[-1][0] - prices[i]) + // = max(-infinity, 0 - prices[i]) + // = -prices[i] + // = max(dp[-1][1], dp[-1][0] - prices[i]) + // = max(-infinity, 0 - prices[i]) + // = -prices[i] + continue; } -return dp[n - 1][0]; ``` -第一题就解决了,但是这样处理 base case 很麻烦,而且注意一下状态转移方程,新状态只和相邻的一个状态有关,其实不用整个 dp 数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1): + + +第一题就解决了,但是这样处理 base case 很麻烦,而且注意一下状态转移方程,新状态只和相邻的一个状态有关,所以可以用前文 [动态规划的降维打击:空间压缩技巧](https://labuladong.online/algo/dynamic-programming/space-optimization/),不需要用整个 `dp` 数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1): ```java -// k == 1 +// 原始版本 +int maxProfit_k_1(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], -prices[i]); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 int maxProfit_k_1(int[] prices) { int n = prices.length; // base case: dp[-1][0] = 0, dp[-1][1] = -infinity @@ -196,11 +317,28 @@ int maxProfit_k_1(int[] prices) { } ``` -两种方式都是一样的,不过这种编程方法简洁很多。但是如果没有前面状态转移方程的引导,是肯定看不懂的。后续的题目,我主要写这种空间复杂度 O(1) 的解法。 -**第二题,k = +infinity** +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ +两种方式都是一样的,不过这种编程方法简洁很多,但是如果没有前面状态转移方程的引导,是肯定看不懂的。后续的题目,你可以对比一下如何把 `dp` 数组的空间优化掉。 + +### 122. 买卖股票的最佳时机 II + +**第二题,看一下力扣第 122 题「买卖股票的最佳时机 II」,也就是 `k` 为正无穷的情况**: -如果 k 为正无穷,那么就可以认为 k 和 k - 1 是一样的。可以这样改写框架: + + +题目还专门强调可以在同一天出售,但我觉得这个条件纯属多余,如果当天买当天卖,那利润当然就是 0,这不是和没有进行交易是一样的吗?这道题的特点在于没有给出交易总数 `k` 的限制,也就相当于 `k` 为正无穷。 + +如果 `k` 为正无穷,那么就可以认为 `k` 和 `k - 1` 是一样的。可以这样改写框架: ```python dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) @@ -215,6 +353,24 @@ dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) 直接翻译成代码: ```java +// 原始版本 +int maxProfit_k_inf(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 int maxProfit_k_inf(int[] prices) { int n = prices.length; int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; @@ -227,11 +383,26 @@ int maxProfit_k_inf(int[] prices) { } ``` -**第三题,k = +infinity with cooldown** -每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可: +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
-``` +### 309. 最佳买卖股票时机含冷冻期 + +**第三题,看力扣第 309 题「最佳买卖股票时机含冷冻期」,也就是 `k` 为正无穷,但含有交易冷冻期的情况**: + + + +和上一道题一样的,只不过每次 `sell` 之后要等一天才能继续交易,只要把这个特点融入上一题的状态转移方程即可: + +```python dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]) 解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。 @@ -240,10 +411,40 @@ dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]) 翻译成代码: ```java +// 原始版本 +int maxProfit_with_cool(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case 1 + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + if (i - 2 == -1) { + // base case 2 + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + // i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = Math.max(dp[i-1][1], -prices[i]); + // dp[i][1] + // = max(dp[i-1][1], dp[-1][0] - prices[i]) + // = max(dp[i-1][1], 0 - prices[i]) + // = max(dp[i-1][1], -prices[i]) + continue; + } + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 int maxProfit_with_cool(int[] prices) { int n = prices.length; int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; - int dp_pre_0 = 0; // 代表 dp[i-2][0] + // 代表 dp[i-2][0] + int dp_pre_0 = 0; for (int i = 0; i < n; i++) { int temp = dp_i_0; dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); @@ -254,20 +455,63 @@ int maxProfit_with_cool(int[] prices) { } ``` -**第四题,k = +infinity with fee** -每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程: +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
-``` +### 714. 买卖股票的最佳时机含手续费 + + + +**第四题,看力扣第 714 题「买卖股票的最佳时机含手续费」,也就是 `k` 为正无穷且考虑交易手续费的情况**: + + + +每次交易要支付手续费,只要把手续费从利润中减去即可,改写方程: + +```python dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee) 解释:相当于买入股票的价格升高了。 在第一个式子里减也是一样的,相当于卖出股票的价格减小了。 ``` -直接翻译成代码: +> [!NOTE] +> 如果直接把 `fee` 放在第一个式子里减,会有一些测试用例无法通过,错误原因是整型溢出而不是思路问题。一种解决方案是把代码中的 `int` 类型都改成 `long` 类型,避免 `int` 的整型溢出。 + +直接翻译成代码,注意状态转移方程改变后 base case 也要做出对应改变: ```java +// 原始版本 +int maxProfit_with_fee(int[] prices, int fee) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i] - fee; + // dp[i][1] + // = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + // = max(dp[-1][1], dp[-1][0] - prices[i] - fee) + // = max(-inf, 0 - prices[i] - fee) + // = -prices[i] - fee + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 int maxProfit_with_fee(int[] prices, int fee) { int n = prices.length; int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; @@ -280,14 +524,31 @@ int maxProfit_with_fee(int[] prices, int fee) { } ``` -**第五题,k = 2** -k = 2 和前面题目的情况稍微不同,因为上面的情况都和 k 的关系不太大。要么 k 是正无穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0 这个 base case 挨得近,最后也没有存在感。 +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ -这道题 k = 2 和后面要讲的 k 是任意正整数的情况中,对 k 的处理就凸显出来了。我们直接写代码,边写边分析原因。 + +### 123. 买卖股票的最佳时机 III + +**第五题,看力扣第 123 题「买卖股票的最佳时机 III」,也就是 `k = 2` 的情况**: + + + +`k = 2` 和前面题目的情况稍微不同,因为上面的情况都和 `k` 的关系不太大:要么 `k` 是正无穷,状态转移和 `k` 没关系了;要么 `k = 1`,跟 `k = 0` 这个 base case 挨得近,最后也没有存在感。 + +这道题 `k = 2` 和后面要讲的 `k` 是任意正整数的情况中,对 `k` 的处理就凸显出来了,我们直接写代码,边写边分析原因。 ```java -原始的动态转移方程,没有可化简的地方 +原始的状态转移方程,没有可化简的地方 dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) ``` @@ -297,8 +558,13 @@ dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) ```java int k = 2; int[][][] dp = new int[n][k + 1][2]; -for (int i = 0; i < n; i++) - if (i - 1 == -1) { /* 处理一下 base case*/ } +for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // 处理 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); } @@ -307,37 +573,80 @@ return dp[n - 1][k][0]; 为什么错误?我这不是照着状态转移方程写的吗? -还记得前面总结的「穷举框架」吗?就是说我们必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中 k 都被化简掉了。比如说第一题,k = 1: +还记得前面总结的「穷举框架」吗?就是说我们必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中 `k` 都被化简掉了。 -「代码截图」 - -这道题由于没有消掉 k 的影响,所以必须要对 k 进行穷举: +比如说第一题,`k = 1` 时的代码框架: ```java -int max_k = 2; -int[][][] dp = new int[n][max_k + 1][2]; +int n = prices.length; +int[][] dp = new int[n][2]; for (int i = 0; i < n; i++) { - for (int k = max_k; k >= 1; k--) { - if (i - 1 == -1) { /*处理 base case */ } - dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); - dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], -prices[i]); +} +return dp[n - 1][0]; +``` + +但当 `k = 2` 时,由于没有消掉 `k` 的影响,所以必须要对 `k` 进行穷举: + +```java +// 原始版本 +int maxProfit_k_2(int[] prices) { + int max_k = 2, n = prices.length; + int[][][] dp = new int[n][max_k + 1][2]; + for (int i = 0; i < n; i++) { + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); + } } + // 穷举了 n × max_k × 2 个状态,正确。 + return dp[n - 1][max_k][0]; } -// 穷举了 n × max_k × 2 个状态,正确。 -return dp[n - 1][max_k][0]; ``` -如果你不理解,可以返回第一点「穷举框架」重新阅读体会一下。 -这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况全部列举出来也可以: +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +> [!NOTE] +> **这里肯定会有读者疑惑,`k` 的 base case 是 0,按理说应该从 `k = 1, k++` 这样穷举状态 `k` 才对?而且如果你真的这样从小到大遍历 `k`,提交发现也是可以的**。 + +这个疑问很正确,因为我们前文 [动态规划答疑篇](https://labuladong.online/algo/dynamic-programming/faq-summary/) 有介绍 `dp` 数组的遍历顺序是怎么确定的,主要是根据 base case,以 base case 为起点,逐步向结果靠近。 + +但为什么我从大到小遍历 `k` 也可以正确提交呢?因为你注意看,`dp[i][k][..]` 不会依赖 `dp[i][k - 1][..]`,而是依赖 `dp[i - 1][k - 1][..]`,而 `dp[i - 1][..][..]`,都是已经计算出来的,所以不管你是 `k = max_k, k--`,还是 `k = 1, k++`,都是可以得出正确答案的。 + +那为什么我使用 `k = max_k, k--` 的方式呢?因为这样符合语义: + +你买股票,初始的「状态」是什么?应该是从第 0 天开始,而且还没有进行过买卖,所以最大交易次数限制 `k` 应该是 `max_k`;而随着「状态」的推移,你会进行交易,那么交易次数上限 `k` 应该不断减少,这样一想,`k = max_k, k--` 的方式是比较合乎实际场景的。 + +当然,这里 `k` 取值范围比较小,所以也可以不用 for 循环,直接把 k = 1 和 2 的情况全部列举出来也可以: ```java -dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]) -dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]) -dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]) -dp[i][1][1] = max(dp[i-1][1][1], -prices[i]) +// 状态转移方程: +// dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]) +// dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]) +// dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]) +// dp[i][1][1] = max(dp[i-1][1][1], -prices[i]) +// 空间复杂度优化版本 int maxProfit_k_2(int[] prices) { + // base case int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE; int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE; for (int price : prices) { @@ -350,50 +659,202 @@ int maxProfit_k_2(int[] prices) { } ``` -有状态转移方程和含义明确的变量名指导,相信你很容易看懂。其实我们可以故弄玄虚,把上述四个变量换成 a, b, c, d。这样当别人看到你的代码时就会大惊失色,对你肃然起敬。 +有状态转移方程和含义明确的变量名指导,相信你很容易看懂。其实我们可以故弄玄虚,把上述四个变量换成 `a, b, c, d`。这样当别人看到你的代码时就会大惊失色,对你肃然起敬。 + +### 188. 买卖股票的最佳时机 IV + +第六题,看力扣第 188 题「买卖股票的最佳时机 IV」,即 `k` 可以是题目给定的任何数的情况: -**第六题,k = any integer** + -有了上一题 k = 2 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。现在想想,交易次数 k 最多有多大呢? +有了上一题 `k = 2` 的铺垫,这题应该和上一题的第一个解法没啥区别,你把上一题的 `k = 2` 换成题目输入的 `k` 就行了。 -一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。 +但试一下发现会出一个内存超限的错误,原来是传入的 `k` 值会非常大,`dp` 数组太大了。那么现在想想,交易次数 `k` 最多有多大呢? -直接把之前的代码重用: +一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 `k` 应该不超过 `n/2`,如果超过,就没有约束作用了,相当于 `k` 没有限制的情况,而这种情况是之前解决过的。 + +所以我们可以直接把之前的代码重用: ```java int maxProfit_k_any(int max_k, int[] prices) { int n = prices.length; - if (max_k > n / 2) + if (n <= 0) { + return 0; + } + if (max_k > n / 2) { + // 复用之前交易次数 k 没有限制的情况 return maxProfit_k_inf(prices); + } + // base case: + // dp[-1][...][0] = dp[...][0][0] = 0 + // dp[-1][...][1] = dp[...][0][1] = -infinity int[][][] dp = new int[n][max_k + 1][2]; + // k = 0 时的 base case + for (int i = 0; i < n; i++) { + dp[i][0][1] = Integer.MIN_VALUE; + dp[i][0][0] = 0; + } + for (int i = 0; i < n; i++) for (int k = max_k; k >= 1; k--) { - if (i - 1 == -1) { /* 处理 base case */ } - dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); - dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); + if (i - 1 == -1) { + // 处理 i = -1 时的 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); } return dp[n - 1][max_k][0]; } ``` + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + 至此,6 道题目通过一个状态转移方程全部解决。 +## 万法归一 + +如果你能看到这里,已经可以给你鼓掌了,初次理解如此复杂的动态规划问题想必消耗了你不少的脑细胞,不过这是值得的,股票系列问题已经属于动态规划问题中较困难的了,如果这些题你都能搞懂,试问,其他那些虾兵蟹将又何足道哉? + +**现在你已经过了九九八十一难中的前八十难,最后我还要再难为你一下,请你实现如下函数**: + +```java +int maxProfit_all_in_one(int max_k, int[] prices, int cooldown, int fee); +``` + +输入股票价格数组 `prices`,你最多进行 `max_k` 次交易,每次交易需要额外消耗 `fee` 的手续费,而且每次交易之后需要经过 `cooldown` 天的冷冻期才能进行下一次交易,请你计算并返回可以获得的最大利润。 + +怎么样,有没有被吓到?如果你直接给别人出一道这样的题目,估计对方要当场吐血,不过我们这样一步步做过来,你应该很容易发现这道题目就是之前我们探讨的几种情况的组合体嘛。 + +所以,我们只要把之前实现的几种代码掺和到一块,**在 base case 和状态转移方程中同时加上 `cooldown` 和 `fee` 的约束就行了**: + +```java +// 同时考虑交易次数的限制、冷冻期和手续费 +int maxProfit_all_in_one(int max_k, int[] prices, int cooldown, int fee) { + int n = prices.length; + if (n <= 0) { + return 0; + } + if (max_k > n / 2) { + // 交易次数 k 没有限制的情况 + return maxProfit_k_inf(prices, cooldown, fee); + } + + int[][][] dp = new int[n][max_k + 1][2]; + // k = 0 时的 base case + for (int i = 0; i < n; i++) { + dp[i][0][1] = Integer.MIN_VALUE; + dp[i][0][0] = 0; + } + + for (int i = 0; i < n; i++) + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // base case 1 + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i] - fee; + continue; + } + + // 包含 cooldown 的 base case + if (i - cooldown - 1 < 0) { + // base case 2 + dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); + // 别忘了减 fee + dp[i][k][1] = Math.max(dp[i-1][k][1], -prices[i] - fee); + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); + // 同时考虑 cooldown 和 fee + dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-cooldown-1][k-1][0] - prices[i] - fee); + } + return dp[n - 1][max_k][0]; +} + +// k 无限制,包含手续费和冷冻期 +int maxProfit_k_inf(int[] prices, int cooldown, int fee) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case 1 + dp[i][0] = 0; + dp[i][1] = -prices[i] - fee; + continue; + } + + // 包含 cooldown 的 base case + if (i - cooldown - 1 < 0) { + // base case 2 + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + // 别忘了减 fee + dp[i][1] = Math.max(dp[i-1][1], -prices[i] - fee); + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + // 同时考虑 cooldown 和 fee + dp[i][1] = Math.max(dp[i - 1][1], dp[i - cooldown - 1][0] - prices[i] - fee); + } + return dp[n - 1][0]; +} +``` + +你可以用这个 `maxProfit_all_in_one` 函数去完成之前讲的 6 道题目,因为我们无法对 `dp` 数组进行优化,所以执行效率上不是最优的,但正确性上肯定是没有问题的。 + +最后总结一下吧,本文给大家讲了如何通过状态转移的方法解决复杂的问题,用一个状态转移方程秒杀了 6 道股票买卖问题,现在回头去看,其实也不算那么可怕对吧? + +关键就在于列举出所有可能的「状态」,然后想想怎么穷举更新这些「状态」。一般用一个多维 `dp` 数组储存这些状态,从 base case 开始向后推进,推进到最后的状态,就是我们想要的答案。想想这个过程,你是不是有点理解「动态规划」这个名词的意义了呢? + +具体到股票买卖问题,我们发现了三个状态,使用了一个三维数组,无非还是穷举 + 更新,不过我们可以说的高大上一点,这叫「三维 DP」,听起来是不是很厉害? + + + + + + + +
+
+引用本文的文章 + + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + +

+ + + + +
+
+引用本文的题目 -**四、最后总结** +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: -本文给大家讲了如何通过状态转移的方法解决复杂的问题,用一个状态转移方程秒杀了 6 道股票买卖问题,现在想想,其实也不算难对吧?这已经属于动态规划问题中较困难的了。 +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer 63. 股票的最大利润](https://leetcode.cn/problems/gu-piao-de-zui-da-li-run-lcof/?show=1) | 🟠 | -关键就在于列举出所有可能的「状态」,然后想想怎么穷举更新这些「状态」。一般用一个多维 dp 数组储存这些状态,从 base case 开始向后推进,推进到最后的状态,就是我们想要的答案。想想这个过程,你是不是有点理解「动态规划」这个名词的意义了呢? +
+
-具体到股票买卖问题,我们发现了三个状态,使用了一个三维数组,无非还是穷举 + 更新,不过我们可以说的高大上一点,这叫「三维 DP」,怕不怕?这个大实话一说,立刻显得你高人一等,名利双收有没有,所以给个在看/分享吧,鼓励一下我。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:动态规划之KMP字符匹配算法](../动态规划系列/动态规划之KMP字符匹配算法.md) -[下一篇:团灭 LeetCode 打家劫舍问题](../动态规划系列/抢房子.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" index 39917bb8e7..9b7fb43724 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" @@ -1,20 +1,50 @@ # 动态规划之子序列问题解题模板 + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1312. Minimum Insertion Steps to Make a String Palindrome](https://leetcode.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/) | [1312. 让字符串成为回文串的最少插入次数](https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/) | 🔴 | +| [516. Longest Palindromic Subsequence](https://leetcode.com/problems/longest-palindromic-subsequence/) | [516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + 子序列问题是常见的算法问题,而且并不好解决。 首先,子序列问题本身就相对子串、子数组更困难一些,因为前者是不连续的序列,而后两者是连续的,就算穷举你都不一定会,更别说求解相关的算法问题了。 -而且,子序列问题很可能涉及到两个字符串,比如前文「最长公共子序列」,如果没有一定的处理经验,真的不容易想出来。所以本文就来扒一扒子序列问题的套路,其实就有两种模板,相关问题只要往这两种思路上想,十拿九稳。 +而且,子序列问题很可能涉及到两个字符串,比如前文 [最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/),如果没有一定的处理经验,真的不容易想出来。所以本文就来扒一扒子序列问题的套路,其实就有两种模板,相关问题只要往这两种思路上想,十拿九稳。 一般来说,这类问题都是让你求一个**最长子序列**,因为最短子序列就是一个字符嘛,没啥可问的。一旦涉及到子序列和最值,那几乎可以肯定,**考察的是动态规划技巧,时间复杂度一般都是 O(n^2)**。 原因很简单,你想想一个字符串,它的子序列有多少种可能?起码是指数级的吧,这种情况下,不用动态规划技巧,还想怎么着? -既然要用动态规划,那就要定义 dp 数组,找状态转移关系。我们说的两种思路模板,就是 dp 数组的定义思路。不同的问题可能需要不同的 dp 数组定义来解决。 +既然要用动态规划,那就要定义 `dp` 数组,找状态转移关系。我们说的两种思路模板,就是 `dp` 数组的定义思路。不同的问题可能需要不同的 `dp` 数组定义来解决。 -### 一、两种思路 +## 一、两种思路 -**1、第一种思路模板是一个一维的 dp 数组**: + + + + + + +**1、第一种思路模板是一个一维的 `dp` 数组**: ```java int n = array.length; @@ -27,13 +57,15 @@ for (int i = 1; i < n; i++) { } ``` -举个我们写过的例子「最长递增子序列」,在这个思路中 dp 数组的定义是: +比如我们写过的 [最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) 和 [最大子数组和](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) 都是这个思路。 -**在子数组 `array[0..i]` 中,我们要求的子序列(最长递增子序列)的长度是 `dp[i]`**。 +在这个思路中 `dp` 数组的定义是: + +**在子数组 `arr[0..i]` 中,以 `arr[i]` 结尾的子序列的长度是 `dp[i]`**。 为啥最长递增子序列需要这种思路呢?前文说得很清楚了,因为这样符合归纳法,可以找到状态转移的关系,这里就不具体展开了。 -**2、第二种思路模板是一个二维的 dp 数组**: +**2、第二种思路模板是一个二维的 `dp` 数组**: ```java int n = arr.length; @@ -49,46 +81,54 @@ for (int i = 0; i < n; i++) { } ``` -这种思路运用相对更多一些,尤其是涉及两个字符串/数组的子序列,比如前文讲的「最长公共子序列」。本思路中 dp 数组含义又分为「只涉及一个字符串」和「涉及两个字符串」两种情况。 +这种思路运用相对更多一些,尤其是涉及两个字符串/数组的子序列时,比如前文讲的 [最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/) 和 [编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/);这种思路也可以用于只涉及一个字符串/数组的情景,比如本文讲的回文子序列问题。 -**2.1 涉及两个字符串/数组时**(比如最长公共子序列),dp 数组的含义如下: +**2.1 涉及两个字符串/数组的场景**,`dp` 数组的定义如下: -**在子数组 `arr1[0..i]` 和子数组 `arr2[0..j]` 中,我们要求的子序列(最长公共子序列)长度为 `dp[i][j]`**。 +**在子数组 `arr1[0..i]` 和子数组 `arr2[0..j]` 中,我们要求的子序列长度为 `dp[i][j]`**。 -**2.2 只涉及一个字符串/数组时**(比如本文要讲的最长回文子序列),dp 数组的含义如下: +**2.2 只涉及一个字符串/数组的场景**,`dp` 数组的定义如下: -**在子数组 `array[i..j]` 中,我们要求的子序列(最长回文子序列)的长度为 `dp[i][j]`**。 +**在子数组 `array[i..j]` 中,我们要求的子序列的长度为 `dp[i][j]`**。 -第一种情况可以参考这两篇旧文:「编辑距离」「公共子序列」 +下面就看看最长回文子序列问题,详解一下第二种情况下如何使用动态规划。 -下面就借最长回文子序列这个问题,详解一下第二种情况下如何使用动态规划。 +## 二、最长回文子序列 -### 二、最长回文子序列 +之前解决了 [最长回文子串](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) 的问题,这次提升难度,看看力扣第 516 题「最长回文子序列」,求最长回文子序列的长度: -之前解决了「最长回文子串」的问题,这次提升难度,求最长回文子序列的长度: +输入一个字符串 `s`,请你找出 `s` 中的最长回文子序列长度,函数签名如下: -![](../pictures/最长回文子序列/1.jpg) +```java +int longestPalindromeSubseq(String s); +``` -我们说这个问题对 dp 数组的定义是:**在子串 `s[i..j]` 中,最长回文子序列的长度为 `dp[i][j]`**。一定要记住这个定义才能理解算法。 +比如说输入 `s = "aecda"`,算法返回 3,因为最长回文子序列是 `"aca"`,长度为 3。 -为啥这个问题要这样定义二维的 dp 数组呢?我们前文多次提到,**找状态转移需要归纳思维,说白了就是如何从已知的结果推出未知的部分**,这样定义容易归纳,容易发现状态转移关系。 +我们对 `dp` 数组的定义是:**在子串 `s[i..j]` 中,最长回文子序列的长度为 `dp[i][j]`**。一定要记住这个定义才能理解算法。 + +为啥这个问题要这样定义二维的 `dp` 数组呢?我在 [最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) 提到,找状态转移需要归纳思维,说白了就是如何从已知的结果推出未知的部分。而这样定义能够进行归纳,容易发现状态转移关系。 具体来说,如果我们想求 `dp[i][j]`,假设你知道了子问题 `dp[i+1][j-1]` 的结果(`s[i+1..j-1]` 中最长回文子序列的长度),你是否能想办法算出 `dp[i][j]` 的值(`s[i..j]` 中,最长回文子序列的长度)呢? -![](../pictures/最长回文子序列/1.jpg) +![](https://labuladong.online/algo/images/lps/1.jpg) 可以!这取决于 `s[i]` 和 `s[j]` 的字符: **如果它俩相等**,那么它俩加上 `s[i+1..j-1]` 中的最长回文子序列就是 `s[i..j]` 的最长回文子序列: -![](../pictures/最长回文子序列/2.jpg) +![](https://labuladong.online/algo/images/lps/2.jpg) **如果它俩不相等**,说明它俩**不可能同时**出现在 `s[i..j]` 的最长回文子序列中,那么把它俩**分别**加入 `s[i+1..j-1]` 中,看看哪个子串产生的回文子序列更长即可: -![](../pictures/最长回文子序列/3.jpg) +![](https://labuladong.online/algo/images/lps/3.jpg) 以上两种情况写成代码就是这样: + + + + ```java if (s[i] == s[j]) // 它俩一定在最长回文子序列中 @@ -98,55 +138,189 @@ else dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); ``` + + 至此,状态转移方程就写出来了,根据 dp 数组的定义,我们要求的就是 `dp[0][n - 1]`,也就是整个 `s` 的最长回文子序列的长度。 -### 三、代码实现 +## 三、代码实现 首先明确一下 base case,如果只有一个字符,显然最长回文子序列长度是 1,也就是 `dp[i][j] = 1 (i == j)`。 因为 `i` 肯定小于等于 `j`,所以对于那些 `i > j` 的位置,根本不存在什么子序列,应该初始化为 0。 -另外,看看刚才写的状态转移方程,想求 `dp[i][j]` 需要知道 `dp[i+1][j-1]`,`dp[i+1][j]`,`dp[i][j-1]` 这三个位置;再看看我们确定的 base case,填入 dp 数组之后是这样: +另外,看看刚才写的状态转移方程,想求 `dp[i][j]` 需要知道 `dp[i+1][j-1]`,`dp[i+1][j]`,`dp[i][j-1]` 这三个位置;再看看我们确定的 base case,填入 `dp` 数组之后是这样: -![](../pictures/最长回文子序列/4.jpg) +![](https://labuladong.online/algo/images/lps/4.jpg) **为了保证每次计算 `dp[i][j]`,左下右方向的位置已经被计算出来,只能斜着遍历或者反着遍历**: -![](../pictures/最长回文子序列/5.jpg) +![](https://labuladong.online/algo/images/lps/5.jpg) + +> [!TIP] +> 关于 `dp` 数组的遍历方向,详情见 [动态规划答疑篇](https://labuladong.online/algo/dynamic-programming/faq-summary/)。 我选择反着遍历,代码如下: -```cpp -int longestPalindromeSubseq(string s) { - int n = s.size(); - // dp 数组全部初始化为 0 - vector> dp(n, vector(n, 0)); - // base case - for (int i = 0; i < n; i++) - dp[i][i] = 1; - // 反着遍历保证正确的状态转移 - for (int i = n - 1; i >= 0; i--) { - for (int j = i + 1; j < n; j++) { - // 状态转移方程 - if (s[i] == s[j]) - dp[i][j] = dp[i + 1][j - 1] + 2; - else - dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); +```java +class Solution { + public int longestPalindromeSubseq(String s) { + int n = s.length(); + // dp 数组全部初始化为 0 + int[][] dp = new int[n][n]; + // base case + for (int i = 0; i < n; i++) { + dp[i][i] = 1; + } + // 反着遍历保证正确的状态转移 + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // 状态转移方程 + if (s.charAt(i) == s.charAt(j)) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); + } + } } + // 整个 s 的最长回文子串长度 + return dp[0][n - 1]; } - // 整个 s 的最长回文子串长度 - return dp[0][n - 1]; } ``` + +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
+ + + 至此,最长回文子序列的问题就解决了。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: +## 四、拓展延伸 + +虽然回文相关的问题没有什么特别广泛的使用场景,但是你会算最长回文子序列之后,一些类似的题目也可以顺手做掉。 + +比如力扣第 1312 题「计算让字符串成为回文串的最少插入次数」: + +输入一个字符串 `s`,你可以在字符串的任意位置插入任意字符。如果要把 `s` 变成回文串,请你计算最少要进行多少次插入? + +函数签名如下: + +```java +int minInsertions(String s); +``` + +比如说输入 `s = "abcea"`,算法返回 2,因为可以给 `s` 插入 2 个字符变成回文串 `"abeceba"` 或者 `"aebcbea"`。如果输入 `s = "aba"`,则算法返回 0,因为 `s` 已经是回文串,不用插入任何字符。 + +这也是一道单字符串的子序列问题,所以我们也可以使用一个二维 `dp` 数组,其中 `dp[i][j]` 的定义如下: + +**对字符串 `s[i..j]`,最少需要进行 `dp[i][j]` 次插入才能变成回文串**。 + +根据 `dp` 数组的定义,base case 就是 `dp[i][i] = 0`,因为单个字符本身就是回文串,不需要插入。 + +然后使用数学归纳法,假设已经计算出了子问题 `dp[i+1][j-1]` 的值了,思考如何推出 `dp[i][j]` 的值: + +![](https://labuladong.online/algo/images/palindrome-insert/1.jpeg) + +实际上和最长回文子序列问题的状态转移方程非常类似,这里也分两种情况: + + + + + +```java +if (s[i] == s[j]) { + // 不需要插入任何字符 + dp[i][j] = dp[i + 1][j - 1]; +} else { + // 把 s[i+1..j] 和 s[i..j-1] 变成回文串,选插入次数较少的 + // 然后还要再插入一个 s[i] 或 s[j],使 s[i..j] 配成回文串 + dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1; +} +``` + + + +最后,我们依然采取倒着遍历 `dp` 数组的方式,写出代码: + +```java +class Solution { + public int minInsertions(String s) { + int n = s.length(); + // dp[i][j] 表示把字符串 s[i..j] 变成回文串的最少插入次数 + // dp 数组全部初始化为 0 + int[][] dp = new int[n][n]; + // 反着遍历保证正确的状态转移 + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // 状态转移方程 + if (s.charAt(i) == s.charAt(j)) { + dp[i][j] = dp[i + 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1; + } + } + } + // 整个 s 的最少插入次数 + return dp[0][n - 1]; + } +} +``` + +至此,这道题也使用子序列解题模板解决了,整体逻辑和最长回文子序列非常相似,那么这个问题是否可以直接复用回文子序列的解法呢? + +其实是可以的,我们甚至都不用写状态转移方程,你仔细想想: + +**我先算出字符串 `s` 中的最长回文子序列,那些不在最长回文子序列中的字符,不就是需要插入的字符吗**? + +所以这道题可以直接复用之前实现的 `longestPalindromeSubseq` 函数: + +```java +class Solution { + // 计算把 s 变成回文串的最少插入次数 + public int minInsertions(String s) { + return s.length() - longestPalindromeSubseq(s); + } + + // 计算 s 中的最长回文子序列长度 + int longestPalindromeSubseq(String s) { + // 见上文 + } +} +``` + +好了,子序列相关的算法就讲到这里,希望对你有启发。 + + + + + + + +
+
+引用本文的文章 + + - [动态规划设计:最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) + - [对动态规划进行降维打击](https://labuladong.online/algo/dynamic-programming/space-optimization/) + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + - [经典动态规划:最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/) + +

+ + + + -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:经典动态规划问题:高楼扔鸡蛋(进阶)](../动态规划系列/高楼扔鸡蛋进阶.md) -[下一篇:动态规划之博弈问题](../动态规划系列/动态规划之博弈问题.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" index eeefc0190d..3b27313c5a 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" @@ -1,20 +1,58 @@ -# 团灭 LeetCode 打家劫舍问题 +# 一个方法团灭 LeetCode 打家劫舍问题 -有读者私下问我 LeetCode 「打家劫舍」系列问题(英文版叫 House Robber)怎么做,我发现这一系列题目的点赞非常之高,是比较有代表性和技巧性的动态规划题目,今天就来聊聊这道题目。 -打家劫舍系列总共有三道,难度设计非常合理,层层递进。第一道是比较标准的动态规划问题,而第二道融入了环形数组的条件,第三道更绝,把动态规划的自底向上和自顶向下解法和二叉树结合起来,我认为很有启发性。如果没做过的朋友,建议学习一下。 + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [198. House Robber](https://leetcode.com/problems/house-robber/) | [198. 打家劫舍](https://leetcode.cn/problems/house-robber/) | 🟠 | +| [213. House Robber II](https://leetcode.com/problems/house-robber-ii/) | [213. 打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/) | 🟠 | +| [337. House Robber III](https://leetcode.com/problems/house-robber-iii/) | [337. 打家劫舍 III](https://leetcode.cn/problems/house-robber-iii/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树系列算法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +今天来讲「打家劫舍」系列问题(英文版叫 House Robber),这个系列是比较有代表性和技巧性的动态规划题目。 + +打家劫舍系列总共有三道,难度设计比较合理,层层递进。第一道是比较标准的动态规划问题,而第二道融入了环形数组的条件,第三道更绝,把动态规划的自底向上和自顶向下解法和二叉树结合起来,我认为很有启发性。 下面,我们从第一道开始分析。 -### House Robber I +## 打家劫舍 I + +力扣第 198 题「打家劫舍」的题目如下: -![title](../pictures/robber/title.png) +街上有一排房屋,用一个包含非负整数的数组 `nums` 表示,每个元素 `nums[i]` 代表第 `i` 间房子中的现金数额。现在你是一名专业盗贼,你希望**尽可能多**的盗窃这些房子中的现金,但是,**相邻的房子不能被同时盗窃**,否则会触发报警器,你就凉凉了。 + +请你写一个算法,计算在不触动报警器的前提下,最多能够盗窃多少现金呢?函数签名如下: ```java -public int rob(int[] nums); +int rob(int[] nums); ``` -题目很容易理解,而且动态规划的特征很明显。我们前文「动态规划详解」做过总结,**解决动态规划问题就是找「状态」和「选择」,仅此而已**。 +比如说输入 `nums=[2,1,7,9,3,1]`,算法返回 12,小偷可以盗窃 `nums[0], nums[3], nums[5]` 三个房屋,得到的现金之和为 2 + 9 + 1 = 12,是最优的选择。 + +题目很容易理解,而且动态规划的特征很明显。我们前文 [动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 做过总结,**解决动态规划问题就是找「状态」和「选择」,仅此而已**。 + + + + + + 假想你就是这个专业强盗,从左到右走过这一排房子,在每间房子前都有两种**选择**:抢或者不抢。 @@ -26,213 +64,301 @@ public int rob(int[] nums); 以上的逻辑很简单吧,其实已经明确了「状态」和「选择」:**你面前房子的索引就是状态,抢和不抢就是选择**。 -![1](../pictures/robber/1.jpg) +![](https://labuladong.online/algo/images/robber/1.jpg) 在两个选择中,每次都选更大的结果,最后得到的就是最多能抢到的 money: ```java -// 主函数 -public int rob(int[] nums) { - return dp(nums, 0); -} -// 返回 nums[start..] 能抢到的最大值 -private int dp(int[] nums, int start) { - if (start >= nums.length) { - return 0; +class Solution { + // 主函数 + public int rob(int[] nums) { + return dp(nums, 0); + } + + // 定义:返回 nums[start..] 能抢到的最大值 + private int dp(int[] nums, int start) { + if (start >= nums.length) { + return 0; + } + + int res = Math.max( + // 不抢,去下家 + dp(nums, start + 1), + // 抢,去下下家 + nums[start] + dp(nums, start + 2) + ); + return res; } - - int res = Math.max( - // 不抢,去下家 - dp(nums, start + 1), - // 抢,去下下家 - nums[start] + dp(nums, start + 2) - ); - return res; } ``` 明确了状态转移,就可以发现对于同一 `start` 位置,是存在重叠子问题的,比如下图: -![2](../pictures/robber/2.jpg) +![](https://labuladong.online/algo/images/robber/2.jpg) 盗贼有多种选择可以走到这个位置,如果每次到这都进入递归,岂不是浪费时间?所以说存在重叠子问题,可以用备忘录进行优化: ```java -private int[] memo; -// 主函数 -public int rob(int[] nums) { - // 初始化备忘录 - memo = new int[nums.length]; - Arrays.fill(memo, -1); - // 强盗从第 0 间房子开始抢劫 - return dp(nums, 0); -} +class Solution { + + private int[] memo; + // 主函数 + public int rob(int[] nums) { + // 初始化备忘录 + memo = new int[nums.length]; + Arrays.fill(memo, -1); + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0); + } -// 返回 dp[start..] 能抢到的最大值 -private int dp(int[] nums, int start) { - if (start >= nums.length) { - return 0; + // 定义:返回 dp[start..] 能抢到的最大值 + private int dp(int[] nums, int start) { + if (start >= nums.length) { + return 0; + } + // 避免重复计算 + if (memo[start] != -1) return memo[start]; + + int res = Math.max( + dp(nums, start + 1), + dp(nums, start + 2) + nums[start] + ); + // 记入备忘录 + memo[start] = res; + return res; } - // 避免重复计算 - if (memo[start] != -1) return memo[start]; - - int res = Math.max(dp(nums, start + 1), - nums[start] + dp(nums, start + 2)); - // 记入备忘录 - memo[start] = res; - return res; } ``` + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + 这就是自顶向下的动态规划解法,我们也可以略作修改,写出**自底向上**的解法: ```java - int rob(int[] nums) { - int n = nums.length; - // dp[i] = x 表示: - // 从第 i 间房子开始抢劫,最多能抢到的钱为 x - // base case: dp[n] = 0 - int[] dp = new int[n + 2]; - for (int i = n - 1; i >= 0; i--) { - dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]); +class Solution { + public int rob(int[] nums) { + int n = nums.length; + // dp[i] = x 表示: + // 从第 i 间房子开始抢劫,最多能抢到的钱为 x + // base case: dp[n] = 0 + int[] dp = new int[n + 2]; + for (int i = n - 1; i >= 0; i--) { + dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]); + } + return dp[0]; } - return dp[0]; } ``` 我们又发现状态转移只和 `dp[i]` 最近的两个状态有关,所以可以进一步优化,将空间复杂度降低到 O(1)。 ```java -int rob(int[] nums) { - int n = nums.length; - // 记录 dp[i+1] 和 dp[i+2] - int dp_i_1 = 0, dp_i_2 = 0; - // 记录 dp[i] - int dp_i = 0; - for (int i = n - 1; i >= 0; i--) { - dp_i = Math.max(dp_i_1, nums[i] + dp_i_2); - dp_i_2 = dp_i_1; - dp_i_1 = dp_i; +class Solution { + public int rob(int[] nums) { + int n = nums.length; + // 记录 dp[i+1] 和 dp[i+2] + int dp_i_1 = 0, dp_i_2 = 0; + // 记录 dp[i] + int dp_i = 0; + for (int i = n - 1; i >= 0; i--) { + dp_i = Math.max(dp_i_1, nums[i] + dp_i_2); + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i; } - return dp_i; } ``` -以上的流程,在我们「动态规划详解」中详细解释过,相信大家都能手到擒来了。我认为很有意思的是这个问题的 follow up,需要基于我们现在的思路做一些巧妙的应变。 +以上的流程,在我们 [动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 中详细解释过,相信大家都能手到擒来了。我认为很有意思的是这个问题的 follow up,需要基于我们现在的思路做一些巧妙的应变。 -### House Robber II +## 打家劫舍 II -这道题目和第一道描述基本一样,强盗依然不能抢劫相邻的房子,输入依然是一个数组,但是告诉你**这些房子不是一排,而是围成了一个圈**。 +力扣第 213 题「打家劫舍 II」和上一道题描述基本一样,强盗依然不能抢劫相邻的房子,输入依然是一个数组,但是告诉你**这些房子不是一排,而是围成了一个圈**。 也就是说,现在第一间房子和最后一间房子也相当于是相邻的,不能同时抢。比如说输入数组 `nums=[2,3,2]`,算法返回的结果应该是 3 而不是 4,因为开头和结尾不能同时被抢。 -这个约束条件看起来应该不难解决,我们前文「单调栈解决 Next Greater Number」说过一种解决环形数组的方案,那么在这个问题上怎么处理呢? +这个约束条件看起来应该不难解决,我们前文 [单调栈问题汇总](https://labuladong.online/algo/data-structure/monotonic-stack/) 说过一种解决环形数组的方案,那么在这个问题上怎么处理呢? 首先,首尾房间不能同时被抢,那么只可能有三种不同情况:要么都不被抢;要么第一间房子被抢最后一间不抢;要么最后一间房子被抢第一间不抢。 -![3](../pictures/robber/3.jpg) +![](https://labuladong.online/algo/images/robber/3.jpg) -那就简单了啊,这三种情况,那种的结果最大,就是最终答案呗!不过,其实我们不需要比较三种情况,只要比较情况二和情况三就行了,**因为这两种情况对于房子的选择余地比情况一大呀,房子里的钱数都是非负数,所以选择余地大,最优决策结果肯定不会小**。 +那就简单了啊,这三种情况,哪种的结果最大,就是最终答案呗!不过,其实我们不需要比较三种情况,只要比较情况二和情况三就行了,**因为这两种情况对于房子的选择余地比情况一大呀,房子里的钱数都是非负数,所以选择余地大,最优决策结果肯定不会小**。 所以只需对之前的解法稍作修改即可: ```java -public int rob(int[] nums) { - int n = nums.length; - if (n == 1) return nums[0]; - return Math.max(robRange(nums, 0, n - 2), - robRange(nums, 1, n - 1)); -} +class Solution { + public int rob(int[] nums) { + int n = nums.length; + if (n == 1) return nums[0]; + return Math.max(robRange(nums, 0, n - 2), + robRange(nums, 1, n - 1)); + } -// 仅计算闭区间 [start,end] 的最优结果 -int robRange(int[] nums, int start, int end) { - int n = nums.length; - int dp_i_1 = 0, dp_i_2 = 0; - int dp_i = 0; - for (int i = end; i >= start; i--) { - dp_i = Math.max(dp_i_1, nums[i] + dp_i_2); - dp_i_2 = dp_i_1; - dp_i_1 = dp_i; + // 定义:返回闭区间 [start,end] 能抢到的最大值 + int robRange(int[] nums, int start, int end) { + int n = nums.length; + int dp_i_1 = 0, dp_i_2 = 0; + int dp_i = 0; + for (int i = end; i >= start; i--) { + dp_i = Math.max(dp_i_1, nums[i] + dp_i_2); + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i; } - return dp_i; } ``` 至此,第二问也解决了。 -### House Robber III +## 打家劫舍 III -第三题又想法设法地变花样了,此强盗发现现在面对的房子不是一排,不是一圈,而是一棵二叉树!房子在二叉树的节点上,相连的两个房子不能同时被抢劫,果然是传说中的高智商犯罪: +力扣第 337 题「打家劫舍 III」又想法设法地变花样了,此强盗发现现在面对的房子不是一排,不是一圈,而是一棵二叉树!房子在二叉树的节点上,相连的两个房子不能同时被抢劫,果然是传说中的高智商犯罪。函数的签名如下: -![title](../pictures/robber/title1.png) +```java +int rob(TreeNode root); +``` + +比如说输入为下图这样一棵二叉树: + +``` + 3 + / \ + 2 3 + \ \ + 3 1 +``` + +算法应该返回 7,因为抢劫第一层和第三层的房子可以得到最高金额 3 + 3 + 1 = 7。 + +如果输入为下图这棵二叉树: + +``` + 3 + / \ + 4 5 + / \ \ + 1 3 1 +``` + +那么算法应该返回 9,如果抢劫第二层的房子可以获得最高金额 4 + 5 = 9。 整体的思路完全没变,还是做抢或者不抢的选择,去收益较大的选择。甚至我们可以直接按这个套路写出代码: ```java -Map memo = new HashMap<>(); -public int rob(TreeNode root) { - if (root == null) return 0; - // 利用备忘录消除重叠子问题 - if (memo.containsKey(root)) - return memo.get(root); - // 抢,然后去下下家 - int do_it = root.val - + (root.left == null ? - 0 : rob(root.left.left) + rob(root.left.right)) - + (root.right == null ? - 0 : rob(root.right.left) + rob(root.right.right)); - // 不抢,然后去下家 - int not_do = rob(root.left) + rob(root.right); - - int res = Math.max(do_it, not_do); - memo.put(root, res); - return res; +class Solution { + Map memo = new HashMap<>(); + public int rob(TreeNode root) { + if (root == null) return 0; + // 利用备忘录消除重叠子问题 + if (memo.containsKey(root)) + return memo.get(root); + // 抢,然后去下下家 + int do_it = root.val + + (root.left == null ? + 0 : rob(root.left.left) + rob(root.left.right)) + + (root.right == null ? + 0 : rob(root.right.left) + rob(root.right.right)); + // 不抢,然后去下家 + int not_do = rob(root.left) + rob(root.right); + + int res = Math.max(do_it, not_do); + memo.put(root, res); + return res; + } } ``` -这道题就解决了,时间复杂度 O(N),`N` 为数的节点数。 -但是这道题让我觉得巧妙的点在于,还有更漂亮的解法。比如下面是我在评论区看到的一个解法: +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +分析下时间复杂度,虽然看这个递归结构似乎是一棵四叉树,但实际上由于备忘录的优化,递归函数做的事情就是还是遍历每个节点,不会多次进入同一个节点,所以时间复杂度是还是 $O(N)$,`N` 为树的节点数。空间复杂度是备忘录的大小,即 $O(N)$。 + +如果对时间/空间复杂度分析有困惑,可以参考 [时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/)。 + +但是这道题巧妙的点在于,还有更漂亮的解法。比如一个读者评论了这样一个解法: ```java -int rob(TreeNode root) { - int[] res = dp(root); - return Math.max(res[0], res[1]); -} +class Solution { + int rob(TreeNode root) { + int[] res = dp(root); + return Math.max(res[0], res[1]); + } -/* 返回一个大小为 2 的数组 arr -arr[0] 表示不抢 root 的话,得到的最大钱数 -arr[1] 表示抢 root 的话,得到的最大钱数 */ -int[] dp(TreeNode root) { - if (root == null) - return new int[]{0, 0}; - int[] left = dp(root.left); - int[] right = dp(root.right); - // 抢,下家就不能抢了 - int rob = root.val + left[0] + right[0]; - // 不抢,下家可抢可不抢,取决于收益大小 - int not_rob = Math.max(left[0], left[1]) - + Math.max(right[0], right[1]); - - return new int[]{not_rob, rob}; + // 返回一个大小为 2 的数组 arr + // arr[0] 表示不抢 root 的话,得到的最大钱数 + // arr[1] 表示抢 root 的话,得到的最大钱数 + int[] dp(TreeNode root) { + if (root == null) + return new int[]{0, 0}; + int[] left = dp(root.left); + int[] right = dp(root.right); + // 抢,下家就不能抢了 + int rob = root.val + left[0] + right[0]; + // 不抢,下家可抢可不抢,取决于收益大小 + int not_rob = Math.max(left[0], left[1]) + + Math.max(right[0], right[1]); + + return new int[]{not_rob, rob}; + } } ``` -时间复杂度 O(N),空间复杂度只有递归函数堆栈所需的空间,不需要备忘录的额外空间。 +时间复杂度还是 $O(N)$,空间复杂度只有递归函数堆栈所需的空间,即树的高度 $O(H)$,不需要备忘录的额外空间。 -你看他和我们的思路不一样,修改了递归函数的定义,略微修改了思路,使得逻辑自洽,依然得到了正确的答案,而且代码更漂亮。这就是我们前文「不同定义产生不同解法」所说过的动态规划问题的一个特性。 +你看他和我们的思路不一样,修改了递归函数的定义,略微修改了思路,使得逻辑自洽,依然得到了正确的答案,而且代码更漂亮。这就是我们前文 [二叉树思维(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 中讲到的后序位置的妙用。 实际上,这个解法比我们的解法运行时间要快得多,虽然算法分析层面时间复杂度是相同的。原因在于此解法没有使用额外的备忘录,减少了数据操作的复杂性,所以实际运行效率会快。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:团灭 LeetCode 股票买卖问题](../动态规划系列/团灭股票问题.md) -[下一篇:动态规划之四键键盘](../动态规划系列/动态规划之四键键盘.md) -[目录](../README.md#目录) \ No newline at end of file +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer II 089. 房屋偷盗](https://leetcode.cn/problems/Gu0c2T/?show=1) | 🟠 | +| - | [剑指 Offer II 090. 环形房屋偷盗](https://leetcode.cn/problems/PzWKhm/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" index 21871b7a1a..bf32b7aed7 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" @@ -1,12 +1,45 @@ -# 动态规划答疑篇 +# 最优子结构原理和 dp 数组遍历方向 -这篇文章就给你讲明白两个问题: + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +> tip:本文有视频版:[动态规划详解进阶](https://www.bilibili.com/video/BV1uv411W73P/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +本文是旧文 [动态规划答疑篇](https://mp.weixin.qq.com/s/qvlfyKBiXVX7CCwWFR-XKg) 的修订版,根据我的不断学习总结以及读者的评论反馈,我给扩展了更多内容,力求使本文成为继 [动态规划核心套路框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 之后的一篇全面答疑文章。以下是正文。 + +这篇文章就给你讲明白以下几个问题: 1、到底什么才叫「最优子结构」,和动态规划什么关系。 -2、为什么动态规划遍历 `dp` 数组的方式五花八门,有的正着遍历,有的倒着遍历,有的斜着遍历。 +2、如何判断一个问题是动态规划问题,即如何看出是否存在重叠子问题。 + +3、为什么经常看到将 `dp` 数组的大小设置为 `n + 1` 而不是 `n`。 + +4、为什么动态规划遍历 `dp` 数组的方式五花八门,有的正着遍历,有的倒着遍历,有的斜着遍历。 + + + -### 一、最优子结构详解 + + + +## 一、最优子结构详解 「最优子结构」是某些问题的一种特定性质,并不是动态规划问题专有的。也就是说,很多问题其实都具有最优子结构,只是其中大部分不具有重叠子问题,所以我们不把它们归为动态规划系列问题而已。 @@ -18,10 +51,14 @@ 再举个例子:假设你们学校有 10 个班,你已知每个班的最大分数差(最高分和最低分的差值)。那么现在我让你计算全校学生中的最大分数差,你会不会算?可以想办法算,但是肯定不能通过已知的这 10 个班的最大分数差推到出来。因为这 10 个班的最大分数差不一定就包含全校学生的最大分数差,比如全校的最大分数差可能是 3 班的最高分和 6 班的最低分之差。 -这次我给你提出的问题就**不符合最优子结构**,因为你没办通过每个班的最优值推出全校的最优值,没办法通过子问题的最优值推出规模更大的问题的最优值。前文「动态规划详解」说过,想满足最优子结,子问题之间必须互相独立。全校的最大分数差可能出现在两个班之间,显然子问题不独立,所以这个问题本身不符合最优子结构。 +这次我给你提出的问题就**不符合最优子结构**,因为你没办通过每个班的最优值推出全校的最优值,没办法通过子问题的最优值推出规模更大的问题的最优值。前文 [动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 说过,想满足最优子结,子问题之间必须互相独立。全校的最大分数差可能出现在两个班之间,显然子问题不独立,所以这个问题本身不符合最优子结构。 **那么遇到这种最优子结构失效情况,怎么办?策略是:改造问题**。对于最大分数差这个问题,我们不是没办法利用已知的每个班的分数差吗,那我只能这样写一段暴力代码: + + + + ```java int result = 0; for (Student a : school) { @@ -33,11 +70,13 @@ for (Student a : school) { return result; ``` + + 改造问题,也就是把问题等价转化:最大分数差,不就等价于最高分数和最低分数的差么,那不就是要求最高和最低分数么,不就是我们讨论的第一个问题么,不就具有最优子结构了么?那现在改变思路,借助最优子结构解决最值问题,再回过头解决最大分数差问题,是不是就高效多了? 当然,上面这个例子太简单了,不过请读者回顾一下,我们做动态规划问题,是不是一直在求各种最值,本质跟我们举的例子没啥区别,无非需要处理一下重叠子问题。 -前文「不同定义不同解法」和「高楼扔鸡蛋进阶」就展示了如何改造问题,不同的最优子结构,可能导致不同的解法和效率。 +前文 [高楼扔鸡蛋问题](https://labuladong.online/algo/dynamic-programming/egg-drop/) 就展示了如何改造问题,不同的最优子结构,可能导致不同的解法和效率。 再举个常见但也十分简单的例子,求一棵二叉树的最大值,不难吧(简单起见,假设节点中的值都是非负数): @@ -57,14 +96,235 @@ int maxVal(TreeNode root) { 动态规划不就是从最简单的 base case 往后推导吗,可以想象成一个链式反应,以小博大。但只有符合最优子结构的问题,才有发生这种链式反应的性质。 -找最优子结构的过程,其实就是证明状态转移方程正确性的过程,方程符合最优子结构就可以写暴力解了,写出暴力解就可以看出有没有重叠子问题了,有则优化,无则 OK。这也是套路,经常刷题的朋友应该能体会。 +找最优子结构的过程,其实就是证明状态转移方程正确性的过程,方程符合最优子结构就可以写暴力解了,写出暴力解就可以看出有没有重叠子问题了,有则优化,无则 OK。这也是套路,经常刷题的读者应该能体会。 + +这里就不举那些正宗动态规划的例子了,读者可以翻翻历史文章,看看状态转移是如何遵循最优子结构的,这个话题就聊到这,下面再来看其他的动态规划迷惑行为。 + + + + + + + +## 二、如何一眼看出重叠子问题 + +经常有读者说: + +看了前文 [动态规划核心套路](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/),我知道了如何一步步优化动态规划问题; -这里就不举那些正宗动态规划的例子了,读者可以翻翻历史文章,看看状态转移是如何遵循最优子结构的,这个话题就聊到这,下面再来看另外个动态规划迷惑行为。 +看了前文 [动态规划设计:数学归纳法](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/),我知道了利用数学归纳法写出暴力解(状态转移方程)。 -### 二、dp 数组的遍历方向 +**但就算我写出了暴力解,我很难判断这个解法是否存在重叠子问题**,从而无法确定是否可以运用备忘录等方法去优化算法效率。 + +对于这个问题,其实我在动态规划系列的文章中写过几次,在这里再统一总结一下吧。 + +**首先,最简单粗暴的方式就是画图,把递归树画出来,看看有没有重复的节点**。 + +比如最简单的例子,[动态规划核心套路](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 中斐波那契数列的递归树: + +![](https://labuladong.online/algo/images/dynamic-programming/1.jpg) + +这棵递归树很明显存在重复的节点,所以我们可以通过备忘录避免冗余计算。 + +但毕竟斐波那契数列问题太简单了,实际的动态规划问题比较复杂,比如二维甚至三维的动态规划,当然也可以画递归树,但不免有些复杂。 + +比如在 [最小路径和问题](https://labuladong.online/algo/dynamic-programming/minimum-path-sum/) 中,我们写出了这样一个暴力解法: + +```java +int dp(int[][] grid, int i, int j) { + if (i == 0 && j == 0) { + return grid[0][0]; + } + if (i < 0 || j < 0) { + return Integer.MAX_VALUE; + } + + return Math.min( + dp(grid, i - 1, j), + dp(grid, i, j - 1) + ) + grid[i][j]; +} +``` + +你不需要读过前文,光看这个函数代码就能看出来,该函数递归过程中参数 `i, j` 在不断变化,即「状态」是 `(i, j)` 的值,你是否可以判断这个解法是否存在重叠子问题呢? + +假设输入的 `i = 8, j = 7`,二维状态的递归树如下图,显然出现了重叠子问题: + +![](https://labuladong.online/algo/images/optimal/2.jpeg) + +**但稍加思考就可以知道,其实根本没必要画图,可以通过递归框架直接判断是否存在重叠子问题**。 + +具体操作就是直接删掉代码细节,抽象出该解法的递归框架: + + + + + +```java +int dp(int[][] grid, int i, int j) { + dp(grid, i - 1, j), // #1 + dp(grid, i, j - 1) // #2 +} +``` + + + +可以看到 `i, j` 的值在不断减小,那么我问你一个问题:如果我想从状态 `(i, j)` 转移到 `(i-1, j-1)`,有几种路径? + +显然有两种路径,可以是 `(i, j) -> #1 -> #2` 或者 `(i, j) -> #2 -> #1`,不止一种,说明 `(i-1, j-1)` 会被多次计算,所以一定存在重叠子问题。 + +再举个稍微复杂的例子,前文 [正则表达式问题](https://labuladong.online/algo/dynamic-programming/regular-expression-matching/) 的暴力解代码: + +```java +boolean dp(String s, int i, String p, int j) { + int m = s.length(), n = p.length(); + // base case + if (j == n) { + return i == m; + } + if (i == m) { + if ((n - j) % 2 == 1) { + return false; + } + for (; j + 1 < n; j += 2) { + if (p.charAt(j + 1) != '*') { + return false; + } + } + return true; + } + + boolean res = false; + + if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2) || dp(s, i + 1, p, j); + } else { + res = dp(s, i + 1, p, j + 1); + } + } else { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2); + } else { + res = false; + } + } + + return res; +} +``` + +代码有些复杂对吧,如果画图的话有些麻烦,但我们不画图,直接忽略所有细节代码和条件分支,只抽象出递归框架: + +```java +boolean dp(String s, int i, String p, int j) { + dp(s, i, p, j + 2); // #1 + dp(s, i + 1, p, j); // #2 + dp(s, i + 1, p, j + 1); // #3 +} +``` + +和上一题一样,这个解法的「状态」也是 `(i, j)` 的值,那么我继续问你问题:如果我想从状态 `(i, j)` 转移到 `(i+2, j+2)`,有几种路径? + +显然,至少有两条路径:`(i, j) -> #1 -> #2 -> #2` 和 `(i, j) -> #3 -> #3`,这就说明这个解法存在巨量重叠子问题。 + +所以,不用画图就知道这个解法也存在重叠子问题,需要用备忘录技巧去优化。 + +## 三、dp 数组的大小设置 + +比如说前文 [编辑距离问题](https://labuladong.online/algo/dynamic-programming/edit-distance/),我首先讲的是自顶向下的递归解法,实现了这样一个 `dp` 函数: + +```java +class Solution { + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 按照 dp 函数的定义,计算 s1 和 s2 的最小编辑距离 + return dp(s1, m - 1, s2, n - 1); + } + + // 定义:s1[0..i] 和 s2[0..j] 的最小编辑距离是 dp(s1, i, s2, j) + int dp(String s1, int i, String s2, int j) { + // 处理 base case + if (i == -1) { + return j + 1; + } + if (j == -1) { + return i + 1; + } + + // 进行状态转移 + if (s1.charAt(i) == s2.charAt(j)) { + return dp(s1, i - 1, s2, j - 1); + } else { + return min( + dp(s1, i, s2, j - 1) + 1, + dp(s1, i - 1, s2, j) + 1, + dp(s1, i - 1, s2, j - 1) + 1 + ); + } + } + + int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } +} +``` + +然后改造成了自底向上的迭代解法: + +```java +class Solution { + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i] 和 s2[0..j] 的最小编辑距离是 dp[i+1][j+1] + int[][] dp = new int[m + 1][n + 1]; + // 初始化 base case + for (int i = 1; i <= m; i++) + dp[i][0] = i; + for (int j = 1; j <= n; j++) + dp[0][j] = j; + + // 自底向上求解 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 进行状态转移 + if (s1.charAt(i-1) == s2.charAt(j-1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = min( + dp[i - 1][j] + 1, + dp[i][j - 1] + 1, + dp[i - 1][j - 1] + 1 + ); + } + } + } + // 按照 dp 数组的定义,存储 s1 和 s2 的最小编辑距离 + return dp[m][n]; + } +} +``` + +这两种解法思路是完全相同的,但就有读者提问,为什么迭代解法中的 `dp` 数组初始化大小要设置为 `int[m+1][n+1]`?为什么 `s1[0..i]` 和 `s2[0..j]` 的最小编辑距离要存储在 `dp[i+1][j+1]` 中,有一位索引偏移? + +能不能模仿 `dp` 函数的定义,把 `dp` 数组初始化为 `int[m][n]`,然后让 `s1[0..i]` 和 `s2[0..j]` 的最小编辑距离要存储在 `dp[i][j]` 中? + +**理论上,你怎么定义都可以,只要根据定义处理好 base case 就可以**。 + +你看 `dp` 函数的定义,`dp(s1, i, s2, j)` 计算 `s1[0..i]` 和 `s2[0..j]` 的编辑距离,那么 `i, j` 等于 -1 时代表空串的 base case,所以函数开头处理了这两种特殊情况。 + +再看 `dp` 数组,你当然也可以定义 `dp[i][j]` 存储 `s1[0..i]` 和 `s2[0..j]` 的编辑距离,但问题是 base case 怎么搞?索引怎么能是 -1 呢? + +所以我们把 `dp` 数组初始化为 `int[m+1][n+1]`,让索引整体偏移一位,把索引 0 留出来作为 base case 表示空串,然后定义 `dp[i+1][j+1]` 存储 `s1[0..i]` 和 `s2[0..j]` 的编辑距离。 + +## 四、dp 数组的遍历方向 我相信读者做动态规问题时,肯定会对 `dp` 数组的遍历顺序有些头疼。我们拿二维 `dp` 数组来举例,有时候我们是正向遍历: + + + + ```java int[][] dp = new int[m][n]; for (int i = 0; i < m; i++) @@ -72,16 +332,28 @@ for (int i = 0; i < m; i++) // 计算 dp[i][j] ``` + + 有时候我们反向遍历: + + + + ```java for (int i = m - 1; i >= 0; i--) for (int j = n - 1; j >= 0; j--) // 计算 dp[i][j] ``` + + 有时候可能会斜向遍历: + + + + ```java // 斜着遍历数组 for (int l = 2; l <= n; l++) { @@ -92,22 +364,28 @@ for (int l = 2; l <= n; l++) { } ``` -甚至更让人迷惑的是,有时候发现正向反向遍历都可以得到正确答案,比如我们在「团灭股票问题」中有的地方就正反皆可。 -那么,如果仔细观察的话可以发现其中的原因的。你只要把住两点就行了: + +甚至更让人迷惑的是,有时候发现正向反向遍历都可以得到正确答案,比如我们在 [团灭股票问题](https://labuladong.online/algo/dynamic-programming/stock-problem-summary/) 中有的地方就正反皆可。 + +如果仔细观察的话可以发现其中的原因,你只要把住两点就行了: **1、遍历的过程中,所需的状态必须是已经计算出来的**。 -**2、遍历的终点必须是存储结果的那个位置**。 +**2、遍历结束后,存储结果的那个位置必须已经被计算出来**。 -下面来距离解释上面两个原则是什么意思。 +下面来具体解释上面两个原则是什么意思。 -比如编辑距离这个经典的问题,详解见前文「编辑距离详解」,我们通过对 `dp` 数组的定义,确定了 base case 是 `dp[..][0]` 和 `dp[0][..]`,最终答案是 `dp[m][n]`;而且我们通过状态转移方程知道 `dp[i][j]` 需要从 `dp[i-1][j]`, `dp[i][j-1]`, `dp[i-1][j-1]` 转移而来,如下图: +比如 [编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/) 这个经典的问题,我们通过对 `dp` 数组的定义,确定了 base case 是 `dp[..][0]` 和 `dp[0][..]`,最终答案是 `dp[m][n]`;而且我们通过状态转移方程知道 `dp[i][j]` 需要从 `dp[i-1][j]`, `dp[i][j-1]`, `dp[i-1][j-1]` 转移而来,如下图: -![](../pictures/最优子结构/1.jpg) +![](https://labuladong.online/algo/images/optimal/1.jpg) 那么,参考刚才说的两条原则,你该怎么遍历 `dp` 数组?肯定是正向遍历: + + + + ```java for (int i = 1; i < m; i++) for (int j = 1; j < n; j++) @@ -115,26 +393,71 @@ for (int i = 1; i < m; i++) // 计算 dp[i][j] ``` + + 因为,这样每一步迭代的左边、上边、左上边的位置都是 base case 或者之前计算过的,而且最终结束在我们想要的答案 `dp[m][n]`。 -再举一例,回文子序列问题,详见前文「子序列问题模板」,我们通过过对 `dp` 数组的定义,确定了 base case 处在中间的对角线,`dp[i][j]` 需要从 `dp[i+1][j]`, `dp[i][j-1]`, `dp[i+1][j-1]` 转移而来,想要求的最终答案是 `dp[0][n-1]`,如下图: +再举一例,回文子序列问题,详见前文 [子序列问题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/),我们通过过对 `dp` 数组的定义,确定了 base case 处在中间的对角线,`dp[i][j]` 需要从 `dp[i+1][j]`, `dp[i][j-1]`, `dp[i+1][j-1]` 转移而来,想要求的最终答案是 `dp[0][n-1]`,如下图: -![](../pictures/最长回文子序列/4.jpg) +![](https://labuladong.online/algo/images/lps/4.jpg) 这种情况根据刚才的两个原则,就可以有两种正确的遍历方式: -![](../pictures/最长回文子序列/5.jpg) +![](https://labuladong.online/algo/images/lps/5.jpg) -要么从左至右斜着遍历,要么从下向上从左到右遍历,这样才能保证每次 `dp[i][j]` 的左边、下边、左下边已经计算完毕,得到正确结果。 +要么从左上至右下斜着遍历,要么从下向上从左到右遍历,这样才能保证每次 `dp[i][j]` 的左边、下边、左下边已经计算完毕,得到正确结果。 现在,你应该理解了这两个原则,主要就是看 base case 和最终结果的存储位置,保证遍历过程中使用的数据都是计算完毕的就行,有时候确实存在多种方法可以得到正确答案,可根据个人口味自行选择。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:动态规划解题框架](../动态规划系列/动态规划详解进阶.md) -[下一篇:回溯算法解题框架](../算法思维系列/回溯算法详解修订版.md) -[目录](../README.md#目录) \ No newline at end of file + + +
+
+引用本文的文章 + + - [一个方法团灭 LeetCode 股票买卖问题](https://labuladong.online/algo/dynamic-programming/stock-problem-summary/) + - [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) + - [动态规划解题套路框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + - [经典动态规划:博弈问题](https://labuladong.online/algo/dynamic-programming/game-theory/) + - [经典动态规划:戳气球](https://labuladong.online/algo/dynamic-programming/burst-balloons/) + - [经典动态规划:正则表达式](https://labuladong.online/algo/dynamic-programming/regular-expression-matching/) + - [经典动态规划:编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [115. Distinct Subsequences](https://leetcode.com/problems/distinct-subsequences/?show=1) | [115. 不同的子序列](https://leetcode.cn/problems/distinct-subsequences/?show=1) | 🔴 | +| [139. Word Break](https://leetcode.com/problems/word-break/?show=1) | [139. 单词拆分](https://leetcode.cn/problems/word-break/?show=1) | 🟠 | +| [221. Maximal Square](https://leetcode.com/problems/maximal-square/?show=1) | [221. 最大正方形](https://leetcode.cn/problems/maximal-square/?show=1) | 🟠 | +| [256. Paint House](https://leetcode.com/problems/paint-house/?show=1)🔒 | [256. 粉刷房子](https://leetcode.cn/problems/paint-house/?show=1)🔒 | 🟠 | +| [343. Integer Break](https://leetcode.com/problems/integer-break/?show=1) | [343. 整数拆分](https://leetcode.cn/problems/integer-break/?show=1) | 🟠 | +| [576. Out of Boundary Paths](https://leetcode.com/problems/out-of-boundary-paths/?show=1) | [576. 出界的路径数](https://leetcode.cn/problems/out-of-boundary-paths/?show=1) | 🟠 | +| [63. Unique Paths II](https://leetcode.com/problems/unique-paths-ii/?show=1) | [63. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/?show=1) | 🟠 | +| [91. Decode Ways](https://leetcode.com/problems/decode-ways/?show=1) | [91. 解码方法](https://leetcode.cn/problems/decode-ways/?show=1) | 🟠 | +| - | [剑指 Offer II 091. 粉刷房子](https://leetcode.cn/problems/JEj789/?show=1) | 🟠 | +| - | [剑指 Offer II 097. 子序列的数目](https://leetcode.cn/problems/21dk04/?show=1) | 🔴 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" deleted file mode 100644 index 98b06af07a..0000000000 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" +++ /dev/null @@ -1,129 +0,0 @@ -# 最长公共子序列 - -最长公共子序列(Longest Common Subsequence,简称 LCS)是一道非常经典的面试题目,因为它的解法是典型的二维动态规划,大部分比较困难的字符串问题都和这个问题一个套路,比如说编辑距离。而且,这个算法稍加改造就可以用于解决其他问题,所以说 LCS 算法是值得掌握的。 - -题目就是让我们求两个字符串的 LCS 长度: - -``` -输入: str1 = "abcde", str2 = "ace" -输出: 3 -解释: 最长公共子序列是 "ace",它的长度是 3 -``` - -肯定有读者会问,为啥这个问题就是动态规划来解决呢?因为子序列类型的问题,穷举出所有可能的结果都不容易,而动态规划算法做的就是穷举 + 剪枝,它俩天生一对儿。所以可以说只要涉及子序列问题,十有八九都需要动态规划来解决,往这方面考虑就对了。 - -下面就来手把手分析一下,这道题目如何用动态规划技巧解决。 - -### 一、动态规划思路 - -**第一步,一定要明确 `dp` 数组的含义**。对于两个字符串的动态规划问题,套路是通用的。 - -比如说对于字符串 `s1` 和 `s2`,一般来说都要构造一个这样的 DP table: - -![](../pictures/LCS/dp.png) - -为了方便理解此表,我们暂时认为索引是从 1 开始的,待会的代码中只要稍作调整即可。其中,`dp[i][j]` 的含义是:对于 `s1[1..i]` 和 `s2[1..j]`,它们的 LCS 长度是 `dp[i][j]`。 - -比如上图的例子,d[2][4] 的含义就是:对于 `"ac"` 和 `"babc"`,它们的 LCS 长度是 2。我们最终想得到的答案应该是 `dp[3][6]`。 - -**第二步,定义 base case。** - -我们专门让索引为 0 的行和列表示空串,`dp[0][..]` 和 `dp[..][0]` 都应该初始化为 0,这就是 base case。 - -比如说,按照刚才 dp 数组的定义,`dp[0][3]=0` 的含义是:对于字符串 `""` 和 `"bab"`,其 LCS 的长度为 0。因为有一个字符串是空串,它们的最长公共子序列的长度显然应该是 0。 - -**第三步,找状态转移方程。** - -这是动态规划最难的一步,不过好在这种字符串问题的套路都差不多,权且借这道题来聊聊处理这类问题的思路。 - -状态转移说简单些就是做选择,比如说这个问题,是求 `s1` 和 `s2` 的最长公共子序列,不妨称这个子序列为 `lcs`。那么对于 `s1` 和 `s2` 中的每个字符,有什么选择?很简单,两种选择,要么在 `lcs` 中,要么不在。 - -![](../pictures/LCS/lcs.png) - -这个「在」和「不在」就是选择,关键是,应该如何选择呢?这个需要动点脑筋:如果某个字符应该在 `lcs` 中,那么这个字符肯定同时存在于 `s1` 和 `s2` 中,因为 `lcs` 是最长**公共**子序列嘛。所以本题的思路是这样: - -用两个指针 `i` 和 `j` 从后往前遍历 `s1` 和 `s2`,如果 `s1[i]==s2[j]`,那么这个字符**一定在 `lcs` 中**;否则的话,`s1[i]` 和 `s2[j]` 这两个字符**至少有一个不在 `lcs` 中**,需要丢弃一个。先看一下递归解法,比较容易理解: - -```python -def longestCommonSubsequence(str1, str2) -> int: - def dp(i, j): - # 空串的 base case - if i == -1 or j == -1: - return 0 - if str1[i] == str2[j]: - # 这边找到一个 lcs 的元素,继续往前找 - return dp(i - 1, j - 1) + 1 - else: - # 谁能让 lcs 最长,就听谁的 - return max(dp(i-1, j), dp(i, j-1)) - - # i 和 j 初始化为最后一个索引 - return dp(len(str1)-1, len(str2)-1) -``` - -对于第一种情况,找到一个 `lcs` 中的字符,同时将 `i` `j` 向前移动一位,并给 `lcs` 的长度加一;对于后者,则尝试两种情况,取更大的结果。 - -其实这段代码就是暴力解法,我们可以通过备忘录或者 DP table 来优化时间复杂度,比如通过前文描述的 DP table 来解决: - -```python -def longestCommonSubsequence(str1, str2) -> int: - m, n = len(str1), len(str2) - # 构建 DP table 和 base case - dp = [[0] * (n + 1) for _ in range(m + 1)] - # 进行状态转移 - for i in range(1, m + 1): - for j in range(1, n + 1): - if str1[i - 1] == str2[j - 1]: - # 找到一个 lcs 中的字符 - dp[i][j] = 1 + dp[i-1][j-1] - else: - dp[i][j] = max(dp[i-1][j], dp[i][j-1]) - - return dp[-1][-1] -``` - -### 二、疑难解答 - -对于 `s1[i]` 和 `s2[j]` 不相等的情况,**至少有一个**字符不在 `lcs` 中,会不会两个字符都不在呢?比如下面这种情况: - -![](../pictures/LCS/1.png) - -所以代码是不是应该考虑这种情况,改成这样: - -```python -if str1[i - 1] == str2[j - 1]: - # ... -else: - dp[i][j] = max(dp[i-1][j], - dp[i][j-1], - dp[i-1][j-1]) -``` - -我一开始也有这种怀疑,其实可以这样改,也能得到正确答案,但是多此一举,因为 `dp[i-1][j-1]` 永远是三者中最小的,max 根本不可能取到它。 - -原因在于我们对 dp 数组的定义:对于 `s1[1..i]` 和 `s2[1..j]`,它们的 LCS 长度是 `dp[i][j]`。 - -![](../pictures/LCS/2.png) - -这样一看,显然 `dp[i-1][j-1]` 对应的 `lcs` 长度不可能比前两种情况大,所以没有必要参与比较。 - -### 三、总结 - -对于两个字符串的动态规划问题,一般来说都是像本文一样定义 DP table,因为这样定义有一个好处,就是容易写出状态转移方程,`dp[i][j]` 的状态可以通过之前的状态推导出来: - -![](../pictures/LCS/3.png) - -找状态转移方程的方法是,思考每个状态有哪些「选择」,只要我们能用正确的逻辑做出正确的选择,算法就能够正确运行。 - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:动态规划之正则表达](../动态规划系列/动态规划之正则表达.md) - -[下一篇:学习算法和刷题的思路指南](../算法思维系列/学习数据结构和算法的高效方法.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\212\266\346\200\201\345\216\213\347\274\251\346\212\200\345\267\247.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\212\266\346\200\201\345\216\213\347\274\251\346\212\200\345\267\247.md" new file mode 100644 index 0000000000..3c0c09a2bb --- /dev/null +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\212\266\346\200\201\345\216\213\347\274\251\346\212\200\345\267\247.md" @@ -0,0 +1,319 @@ +# 对动态规划进行降维打击 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +> [!NOTE] +> 空间压缩并不难,主要用来优化某些动态规划问题的空间复杂度。但是一般的笔试中对空间的要求并不高,即便不使用这个优化技巧也能通过,所以我个人认为状态压缩并不是必须掌握的技巧,有兴趣的读者可以仔细学习理解一下。 + +我们号之前写过十几篇动态规划文章,可以说动态规划技巧对于算法效率的提升非常可观,一般来说都能把指数级和阶乘级时间复杂度的算法优化成 O(N^2),堪称算法界的二向箔,把各路魑魅魍魉统统打成二次元。 + +但是,动态规划求解的过程中也是可以进行阶段性优化的,如果你认真观察某些动态规划问题的状态转移方程,就能够把它们解法的空间复杂度进一步降低,由 O(N^2) 降低到 O(N)。 + + + + + + + +> [!NOTE] +> 之前我在本文中误用了「状态压缩」这个词,有读者指出「状态压缩」这个词的含义是把多个状态通过二进制运算用一个整数表示出来,从而减少 `dp` 数组的维度。而本文描述的优化方式是通过观察状态转移方程的依赖关系,从而减少 `dp` 数组的维度,确实和「状态压缩」有所区别。所以严谨起见,我把原来文章中的「状态压缩」都改为了「空间压缩」,避免名词的误用。 + +能够使用空间压缩技巧的动态规划都是二维 `dp` 问题,**你看它的状态转移方程,如果计算状态 `dp[i][j]` 需要的都是 `dp[i][j]` 相邻的状态,那么就可以使用空间压缩技巧**,将二维的 `dp` 数组转化成一维,将空间复杂度从 O(N^2) 降低到 O(N)。 + +什么叫「和 `dp[i][j]` 相邻的状态」呢,比如前文 [最长回文子序列](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) 中,最终的代码如下: + +```java +int longestPalindromeSubseq(String s) { + int n = s.length(); + // dp 数组全部初始化为 0 + int[][] dp = new int[n][n]; + // base case + for (int i = 0; i < n; i++) { + dp[i][i] = 1; + } + // 反着遍历保证正确的状态转移 + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // 状态转移方程 + if (s.charAt(i) == s.charAt(j)) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); + } + } + } + // 整个 s 的最长回文子串长度 + return dp[0][n - 1]; +} +``` + +> [!TIP] +> 我们本文不探讨如何推状态转移方程,只探讨对二维 DP 问题进行空间压缩的技巧。技巧都是通用的,所以如果你没看过前文,不明白这段代码的逻辑也无妨,完全不会阻碍你学会空间压缩。 + +你看我们对 `dp[i][j]` 的更新,其实只依赖于 `dp[i+1][j-1], dp[i][j-1], dp[i+1][j]` 这三个状态: + +![](https://labuladong.online/algo/images/space-optimal/1.jpeg) + +这就叫和 `dp[i][j]` 相邻,反正你计算 `dp[i][j]` 只需要这三个相邻状态,其实根本不需要那么大一个二维的 dp table 对不对?**空间压缩的核心思路就是,将二维数组「投影」到一维数组**: + +![](https://labuladong.online/algo/images/space-optimal/2.jpeg) + +「投影」这个词应该比较形象吧,说白了就是希望让一维数组发挥原来二维数组的作用。 + +思路很直观,但是也有一个明显的问题,图中 `dp[i][j-1]` 和 `dp[i+1][j-1]` 这两个状态处在同一列,而一维数组中只能容下一个,那么他俩投影到一维必然有一个会被另一个覆盖掉,我还怎么计算 `dp[i][j]` 呢? + +这就是空间压缩的难点,下面就来分析解决这个问题,还是拿「最长回文子序列」问题举例,它的状态转移方程主要逻辑就是如下这段代码: + + + + + +```java +for (int i = n - 2; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // 状态转移方程 + if (s.charAt(i) == s.charAt(j)) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); + } + } +} +``` + + + +回想上面的图,「投影」其实就是把多行变成一行,所以想把二维 `dp` 数组压缩成一维,一般来说是把第一个维度,也就是 `i` 这个维度去掉,只剩下 `j` 这个维度。**压缩后的一维 `dp` 数组就是之前二维 `dp` 数组的 `dp[i][..]` 那一行**。 + +我们先将上述代码进行改造,直接无脑去掉 `i` 这个维度,把 `dp` 数组变成一维: + + + + + +```java +for (int i = n - 2; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // 在这里,一维 dp 数组中的数是什么? + if (s.charAt(i) == s.charAt(j)) { + dp[j] = dp[j - 1] + 2; + } else { + dp[j] = Math.max(dp[j], dp[j - 1]); + } + } +} +``` + + + +上述代码的一维 `dp` 数组只能表示二维 `dp` 数组的一行 `dp[i][..]`。但是我们想得到 `dp[i+1][j-1], dp[i][j-1], dp[i+1][j]` 这几个必要的的值进行状态转移。 + +因此,我们要先来思考两个问题: + +1、在对 `dp[j]` 赋新值之前,`dp[j]` 对应着二维 `dp` 数组中的什么位置? + +2、`dp[j-1]` 对应着二维 `dp` 数组中的什么位置? + +**对于问题 1,在对 `dp[j]` 赋新值之前,`dp[j]` 的值就是外层 for 循环上一次迭代算出来的值,也就是对应二维 `dp` 数组中 `dp[i+1][j]` 的位置**。 + +**对于问题 2,`dp[j-1]` 的值就是内层 for 循环上一次迭代算出来的值,也就是对应二维 `dp` 数组中 `dp[i][j-1]` 的位置**。 + +那么问题已经解决了一大半了,只剩下二维 `dp` 数组中的 `dp[i+1][j-1]` 这个状态我们不能直接从一维 `dp` 数组中得到: + + + + + +```java +for (int i = n - 2; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + if (s.charAt(i) == s.charAt(j)) { + // dp[i][j] = dp[i+1][j-1] + 2; + dp[j] = ?? + 2; + } else { + // dp[i][j] = max(dp[i+1][j], dp[i][j-1]); + dp[j] = Math.max(dp[j], dp[j - 1]); + } + } +} +``` + + + +因为 for 循环遍历 `i` 和 `j` 的顺序为从左向右,从下向上,所以可以发现,在更新一维 `dp` 数组的时候,`dp[i+1][j-1]` 会被 `dp[i][j-1]` 覆盖掉,图中标出了这四个位置被遍历到的次序: + +![](https://labuladong.online/algo/images/space-optimal/3.jpeg) + +**那么如果我们想得到 `dp[i+1][j-1]`,就必须在它被覆盖之前用一个临时变量 `temp` 把它存起来,并把这个变量的值保留到计算 `dp[i][j]` 的时候**。为了达到这个目的,结合上图,我们可以这样写代码: + + + + + +```java +for (int i = n - 2; i >= 0; i--) { + // 存储 dp[i+1][j-1] 的变量 + int pre = 0; + for (int j = i + 1; j < n; j++) { + int temp = dp[j]; + if (s.charAt(i) == s.charAt(j)) { + // dp[i][j] = dp[i+1][j-1] + 2; + dp[j] = pre + 2; + } else { + dp[j] = Math.max(dp[j], dp[j - 1]); + } + // 到下一轮循环,pre 就是 dp[i+1][j-1] 了 + pre = temp; + } +} +``` + + + +别小看这段代码,这是一维 `dp` 最精妙的地方,会者不难,难者不会。为了清晰起见,我用具体的数值来拆解这个逻辑: + +假设现在 `i = 5, j = 7` 且 `s[5] == s[7]`,那么现在会进入下面这个逻辑对吧: + + + + + +```java +for (int i = 5; i--) { + for (int j = 7; j++) { + if (s[5] == s[7]) { + // dp[5][7] = dp[i+1][j-1] + 2; + dp[7] = pre + 2; + } + } +} +``` + + + +我问你这个 `pre` 变量是什么?是内层 for 循环上一次迭代的 `temp` 值。 + +那我再问你**内层** for 循环上一次迭代的 `temp` 值是什么?是 `dp[j-1]` 也就是 `dp[6]`,但请注意,这是**外层** for 循环**上一次迭代**对应的 `dp[6]`,不是现在的 `dp[6]`。 + +这个要对应二维数组的索引来理解。你现在的 `dp[6]` 是二维 `dp` 数组中的 `dp[i][6] = dp[5][6]`,而人家这个 `temp` 是二维 `dp` 数组中的 `dp[i+1][6] = dp[6][6]`。 + +也就是说,`pre` 变量就是 `dp[i+1][j-1] = dp[6][6]`,也就是我们想要的结果。 + +那么现在我们成功对状态转移方程进行了降维打击,算是最硬的的骨头啃掉了,但注意到我们还有 base case 要处理呀: + +```java +// dp 数组全部初始化为 0 +int[][] dp = new int[n][n]; +// base case +for (int i = 0; i < n; i++) { + dp[i][i] = 1; +} +``` + +如何把 base case 也打成一维呢?很简单,记住空间压缩就是投影,我们把 base case 投影到一维看看: + +![](https://labuladong.online/algo/images/space-optimal/4.jpeg) + +二维 `dp` 数组中的 base case 全都落入了一维 `dp` 数组,不存在冲突和覆盖,所以说我们直接这样写代码就行了: + +```java +// base case:一维 dp 数组全部初始化为 1 +int[] dp = new int[n]; +Arrays.fill(dp, 1); +``` + +至此,我们把 base case 和状态转移方程都进行了降维,实际上已经写出完整代码了: + +```java +class Solution { + public int longestPalindromeSubseq(String s) { + int n = s.length(); + // base case:一维 dp 数组全部初始化为 1 + int[] dp = new int[n]; + Arrays.fill(dp, 1); + + for (int i = n - 2; i >= 0; i--) { + int pre = 0; + for (int j = i + 1; j < n; j++) { + int temp = dp[j]; + // 状态转移方程 + if (s.charAt(i) == s.charAt(j)) + dp[j] = pre + 2; + else + dp[j] = Math.max(dp[j], dp[j - 1]); + pre = temp; + } + } + return dp[n - 1]; + } +} +``` + +本文就结束了,不过空间压缩技巧再牛逼,也是基于常规动态规划思路之上的。 + +你也看到了,使用空间压缩技巧对二维 `dp` 数组进行降维打击之后,解法代码的可读性变得非常差了,如果直接看这种解法,任何人都是一脸懵逼的。算法的优化就是这么一个过程,先写出可读性很好的暴力递归算法,然后尝试运用动态规划技巧优化重叠子问题,最后尝试用空间压缩技巧优化空间复杂度。 + +也就是说,你最起码能够熟练运用我们前文 [动态规划框架套路详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 的套路找出状态转移方程,写出一个正确的动态规划解法,然后才有可能观察状态转移的情况,分析是否可能使用空间压缩技巧来优化。 + +希望读者能够稳扎稳打,层层递进,对于这种比较极限的优化,不做也罢。毕竟套路存于心,走遍天下都不怕! + + + + + + + +
+
+引用本文的文章 + + - [一个方法团灭 LeetCode 股票买卖问题](https://labuladong.online/algo/dynamic-programming/stock-problem-summary/) + - [动态规划之最小路径和](https://labuladong.online/algo/dynamic-programming/minimum-path-sum/) + - [动态规划解题套路框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + - [动态规划设计:最大子数组](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [经典动态规划:子集背包问题](https://labuladong.online/algo/dynamic-programming/knapsack2/) + - [经典动态规划:完全背包问题](https://labuladong.online/algo/dynamic-programming/knapsack3/) + - [经典动态规划:最长公共子序列](https://labuladong.online/algo/dynamic-programming/longest-common-subsequence/) + - [经典动态规划:高楼扔鸡蛋](https://labuladong.online/algo/dynamic-programming/egg-drop/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [63. Unique Paths II](https://leetcode.com/problems/unique-paths-ii/?show=1) | [63. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" index 4e4146bb39..75691018f9 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" @@ -1,47 +1,103 @@ -# 编辑距离 +# 经典动态规划:编辑距离 -前几天看了一份鹅场的面试题,算法部分大半是动态规划,最后一题就是写一个计算编辑距离的函数,今天就专门写一篇文章来探讨一下这个问题。 -我个人很喜欢编辑距离这个问题,因为它看起来十分困难,解法却出奇得简单漂亮,而且它是少有的比较实用的算法(是的,我承认很多算法问题都不太实用)。下面先来看下题目: -![](../pictures/editDistance/title.png) +![](https://labuladong.online/algo/images/souyisou1.png) -为什么说这个问题难呢,因为显而易见,它就是难,让人手足无措,望而生畏。 +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -为什么说它实用呢,因为前几天我就在日常生活中用到了这个算法。之前有一篇公众号文章由于疏忽,写错位了一段内容,我决定修改这部分内容让逻辑通顺。但是公众号文章最多只能修改 20 个字,且只支持增、删、替换操作(跟编辑距离问题一模一样),于是我就用算法求出了一个最优方案,只用了 16 步就完成了修改。 + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [72. Edit Distance](https://leetcode.com/problems/edit-distance/) | [72. 编辑距离](https://leetcode.cn/problems/edit-distance/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树系列算法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + + +> tip:本文有视频版:[编辑距离详解动态规划](https://www.bilibili.com/video/BV1uv411W73P/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +前几天看了一份鹅厂的面试题,算法部分大半是动态规划,最后一题就是写一个计算编辑距离的函数,今天就专门写一篇文章来探讨一下这个问题。 + +力扣第 72 题「编辑距离」就是这个问题,先看下题目: + + + + + + + +```java +// 函数签名如下 +int minDistance(String s1, String s2) +``` + +对于没有接触过动态规划问题的读者来说,这道题还是有一定难度的,是不是感觉完全无从下手? + +但这个问题本身还是比较实用的,我曾经就在日常生活中用到过这个算法。之前有一篇公众号文章由于疏忽,写错位了一段内容,我决定修改这部分内容让逻辑通顺。但是公众号文章最多只能修改 20 个字,且只支持增、删、替换操作(跟编辑距离问题一模一样),于是我就用算法求出了一个最优方案,只用了 16 步就完成了修改。 再比如高大上一点的应用,DNA 序列是由 A,G,C,T 组成的序列,可以类比成字符串。编辑距离可以衡量两个 DNA 序列的相似度,编辑距离越小,说明这两段 DNA 越相似,说不定这俩 DNA 的主人是远古近亲啥的。 下面言归正传,详细讲解一下编辑距离该怎么算,相信本文会让你有收获。 -### 一、思路 + + + + + + +## 一、思路 编辑距离问题就是给我们两个字符串 `s1` 和 `s2`,只能用三种操作,让我们把 `s1` 变成 `s2`,求最少的操作数。需要明确的是,不管是把 `s1` 变成 `s2` 还是反过来,结果都是一样的,所以后文就以 `s1` 变成 `s2` 举例。 -前文「最长公共子序列」说过,**解决两个字符串的动态规划问题,一般都是用两个指针 `i,j` 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模**。 +> [!TIP] +> 解决两个字符串的动态规划问题,一般都是用两个指针 `i, j` 分别指向两个字符串的头部或尾部,然后尝试写状态转移方程。 +> +> 比方说让 `i, j` 分别指向两个字符串的尾部,把 `dp[i], dp[j]` 定义为 `s1[0..i], s2[0..j]` 子串的编辑距离,那么 `i, j` 一步步往前移动的过程,就是问题规模(子串长度)逐步减小的过程。 +> +> 当然,你想让让 `i, j` 分别指向字符串头部,然后一步步往后移动也可以,本质上并无区别,只要改一下 `dp` 函数/数组的定义即可。 -设两个字符串分别为 "rad" 和 "apple",为了把 `s1` 变成 `s2`,算法会这样进行: +设两个字符串分别为 `"rad"` 和 `"apple"`,让 `i, j` 两个指针分别指向 `s1, s2` 的尾部,为了把 `s1` 变成 `s2`,算法会这样进行: -![](../pictures/editDistance/edit.gif) -![](../pictures/editDistance/1.jpg) +![](https://labuladong.online/algo/images/editDistance/edit.gif) + +![](https://labuladong.online/algo/images/editDistance/1.jpg) 请记住这个 GIF 过程,这样就能算出编辑距离。关键在于如何做出正确的操作,稍后会讲。 根据上面的 GIF,可以发现操作不只有三个,其实还有第四个操作,就是什么都不要做(skip)。比如这个情况: -![](../pictures/editDistance/2.jpg) +![](https://labuladong.online/algo/images/editDistance/2.jpg) -因为这两个字符本来就相同,为了使编辑距离最小,显然不应该对它们有任何操作,直接往前移动 `i,j` 即可。 +因为这两个字符本来就相同,为了使编辑距离最小,显然不应该对它们有任何操作,直接往前移动 `i, j` 即可。 还有一个很容易处理的情况,就是 `j` 走完 `s2` 时,如果 `i` 还没走完 `s1`,那么只能用删除操作把 `s1` 缩短为 `s2`。比如这个情况: -![](../pictures/editDistance/3.jpg) +![](https://labuladong.online/algo/images/editDistance/3.jpg) 类似的,如果 `i` 走完 `s1` 时 `j` 还没走完了 `s2`,那就只能用插入操作把 `s2` 剩下的字符全部插入 `s1`。等会会看到,这两种情况就是算法的 **base case**。 -下面详解一下如何将思路转换成代码,坐稳,要发车了。 +下面详解一下如何将思路转换成代码。 + + -### 二、代码详解 + + + + +## 二、代码详解 先梳理一下之前的思路: @@ -60,43 +116,57 @@ else: 替换(replace) ``` -有这个框架,问题就已经解决了。读者也许会问,这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧,理解需要点技巧,先看下代码: - -```python -def minDistance(s1, s2) -> int: +有这个框架,问题就已经解决了。读者也许会问,这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧,先看下暴力解法代码: - def dp(i, j): - # base case - if i == -1: return j + 1 - if j == -1: return i + 1 - - if s1[i] == s2[j]: - return dp(i - 1, j - 1) # 啥都不做 - else: - return min( - dp(i, j - 1) + 1, # 插入 - dp(i - 1, j) + 1, # 删除 - dp(i - 1, j - 1) + 1 # 替换 - ) - - # i,j 初始化指向最后一个索引 - return dp(len(s1) - 1, len(s2) - 1) +```java +class Solution { + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // i,j 初始化指向最后一个索引 + return dp(s1, m - 1, s2, n - 1); + } + + // 定义:返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 + int dp(String s1, int i, String s2, int j) { + // base case + if (i == -1) return j + 1; + if (j == -1) return i + 1; + + if (s1.charAt(i) == s2.charAt(j)) { + // 啥都不做 + return dp(s1, i - 1, s2, j - 1); + } + return min( + // 插入 + dp(s1, i, s2, j - 1) + 1, + // 删除 + dp(s1, i - 1, s2, j) + 1, + // 替换 + dp(s1, i - 1, s2, j - 1) + 1 + ); + } + + int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } +} ``` 下面来详细解释一下这段递归代码,base case 应该不用解释了,主要解释一下递归部分。 -都说递归代码的可解释性很好,这是有道理的,只要理解函数的定义,就能很清楚地理解算法的逻辑。我们这里 dp(i, j) 函数的定义是这样的: +都说递归代码的可解释性很好,这是有道理的,只要理解函数的定义,就能很清楚地理解算法的逻辑。我们这里 `dp` 函数的定义是这样的: -```python -def dp(i, j) -> int -# 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 +```java +// 定义:返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 +int dp(String s1, int i, String s2, int j) ``` **记住这个定义**之后,先来看这段代码: ```python if s1[i] == s2[j]: - return dp(i - 1, j - 1) # 啥都不做 + # 啥都不做 + return dp(s1, i - 1, s2, j - 1) # 解释: # 本来就相等,不需要任何操作 # s1[0..i] 和 s2[0..j] 的最小编辑距离等于 @@ -104,129 +174,187 @@ if s1[i] == s2[j]: # 也就是说 dp(i, j) 等于 dp(i-1, j-1) ``` -如果 `s1[i]!=s2[j]`,就要对三个操作递归了,稍微需要点思考: +如果 `s1[i] != s2[j]`,就要对三个操作递归了,稍微需要点思考: ```python -dp(i, j - 1) + 1, # 插入 +# 插入 +dp(s1, i, s2, j - 1) + 1, # 解释: # 我直接在 s1[i] 插入一个和 s2[j] 一样的字符 # 那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比 # 别忘了操作数加一 ``` -![](../pictures/editDistance/insert.gif) +![](https://labuladong.online/algo/images/editDistance/insert.gif) ```python -dp(i - 1, j) + 1, # 删除 +# 删除 +dp(s1, i - 1, s2, j) + 1, # 解释: # 我直接把 s[i] 这个字符删掉 # 前移 i,继续跟 j 对比 # 操作数加一 ``` -![](../pictures/editDistance/delete.gif) +![](https://labuladong.online/algo/images/editDistance/delete.gif) ```python -dp(i - 1, j - 1) + 1 # 替换 +# 替换 +dp(s1, i - 1, s2, j - 1) + 1 # 解释: # 我直接把 s1[i] 替换成 s2[j],这样它俩就匹配了 # 同时前移 i,j 继续对比 # 操作数加一 ``` -![](../pictures/editDistance/replace.gif) +![](https://labuladong.online/algo/images/editDistance/replace.gif) + + 现在,你应该完全理解这段短小精悍的代码了。还有点小问题就是,这个解法是暴力解法,存在重叠子问题,需要用动态规划技巧来优化。 -**怎么能一眼看出存在重叠子问题呢**?前文「动态规划之正则表达式」有提过,这里再简单提一下,需要抽象出本文算法的递归框架: +**怎么能一眼看出存在重叠子问题呢**?我在 [动态规划答疑篇](https://labuladong.online/algo/dynamic-programming/faq-summary/) 有讲过,这里再简单提一下,需要抽象出本文算法的递归框架: -```python -def dp(i, j): - dp(i - 1, j - 1) #1 - dp(i, j - 1) #2 - dp(i - 1, j) #3 +```java +int dp(i, j) { + dp(i - 1, j - 1); // #1 + dp(i, j - 1); // #2 + dp(i - 1, j); // #3 +} ``` 对于子问题 `dp(i-1, j-1)`,如何通过原问题 `dp(i, j)` 得到呢?有不止一条路径,比如 `dp(i, j) -> #1` 和 `dp(i, j) -> #2 -> #3`。一旦发现一条重复路径,就说明存在巨量重复路径,也就是重叠子问题。 -### 三、动态规划优化 +## 三、动态规划优化 -对于重叠子问题呢,前文「动态规划详解」详细介绍过,优化方法无非是备忘录或者 DP table。 +对于重叠子问题呢,前文 [动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 详细介绍过,优化方法无非是给递归解法加备忘录,或者把动态规划过程用 DP table 迭代实现,下面逐个来讲。 -备忘录很好加,原来的代码稍加修改即可: +### 备忘录解法 -```python -def minDistance(s1, s2) -> int: +既然暴力递归解法都写出来了,备忘录是很容易加的,原来的代码稍加修改即可: - memo = dict() # 备忘录 - def dp(i, j): - if (i, j) in memo: - return memo[(i, j)] - ... +```java +class Solution { + // 备忘录 + int[][] memo; - if s1[i] == s2[j]: - memo[(i, j)] = ... - else: - memo[(i, j)] = ... - return memo[(i, j)] - - return dp(len(s1) - 1, len(s2) - 1) + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 备忘录初始化为特殊值,代表还未计算 + memo = new int[m][n]; + for (int[] row : memo) { + Arrays.fill(row, -1); + } + return dp(s1, m - 1, s2, n - 1); + } + + int dp(String s1, int i, String s2, int j) { + if (i == -1) return j + 1; + if (j == -1) return i + 1; + // 查备忘录,避免重叠子问题 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 状态转移,结果存入备忘录 + if (s1.charAt(i) == s2.charAt(j)) { + memo[i][j] = dp(s1, i - 1, s2, j - 1); + } else { + memo[i][j] = min( + dp(s1, i, s2, j - 1) + 1, + dp(s1, i - 1, s2, j) + 1, + dp(s1, i - 1, s2, j - 1) + 1 + ); + } + return memo[i][j]; + } + + int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } +} ``` -**主要说下 DP table 的解法**: +### DP table 解法 -首先明确 dp 数组的含义,dp 数组是一个二维数组,长这样: +主要说下 DP table 的解法,我们需要定义一个 `dp` 数组,然后在这个数组上执行状态转移方程。 + +首先明确 `dp` 数组的含义,由于本题有两个状态(索引 `i` 和 `j`),所以`dp` 数组是一个二维数组,大概长这样: + +![](https://labuladong.online/algo/images/editDistance/dp.jpg) + +状态转移和递归解法相同,`dp[..][0]` 和 `dp[0][..]` 对应 base case,`dp[i][j]` 的含义和之前 `dp` 函数的定义类似: -![](../pictures/editDistance/dp.jpg) -有了之前递归解法的铺垫,应该很容易理解。`dp[..][0]` 和 `dp[0][..]` 对应 base case,`dp[i][j]` 的含义和之前的 dp 函数类似: -```python -def dp(i, j) -> int -# 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 + + +```java +int dp(String s1, int i, String s2, int j) +// 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 dp[i-1][j-1] -# 存储 s1[0..i] 和 s2[0..j] 的最小编辑距离 +// 存储 s1[0..i] 和 s2[0..j] 的最小编辑距离 ``` -dp 函数的 base case 是 `i,j` 等于 -1,而数组索引至少是 0,所以 dp 数组会偏移一位。 -既然 dp 数组和递归 dp 函数含义一样,也就可以直接套用之前的思路写代码,**唯一不同的是,DP table 是自底向上求解,递归解法是自顶向下求解**: + +`dp` 函数的 base case 是 `i, j` 等于 -1,而数组索引至少是 0,所以 `dp` 数组会偏移一位。 + +既然 `dp` 数组和递归 `dp` 函数含义一样,也就可以直接套用之前的思路写代码,**唯一不同的是,递归解法是自顶向下求解(从原问题开始,逐步分解到 base case),DP table 是自底向上求解(从 base case 开始,向原问题推演)**: ```java -int minDistance(String s1, String s2) { - int m = s1.length(), n = s2.length(); - int[][] dp = new int[m + 1][n + 1]; - // base case - for (int i = 1; i <= m; i++) - dp[i][0] = i; - for (int j = 1; j <= n; j++) - dp[0][j] = j; - // 自底向上求解 - for (int i = 1; i <= m; i++) +class Solution { + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i] 和 s2[0..j] 的最小编辑距离是 dp[i+1][j+1] + int[][] dp = new int[m + 1][n + 1]; + // base case + for (int i = 1; i <= m; i++) + dp[i][0] = i; for (int j = 1; j <= n; j++) - if (s1.charAt(i-1) == s2.charAt(j-1)) - dp[i][j] = dp[i - 1][j - 1]; - else - dp[i][j] = min( - dp[i - 1][j] + 1, - dp[i][j - 1] + 1, - dp[i-1][j-1] + 1 - ); - // 储存着整个 s1 和 s2 的最小编辑距离 - return dp[m][n]; -} - -int min(int a, int b, int c) { - return Math.min(a, Math.min(b, c)); + dp[0][j] = j; + // 自底向上求解 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (s1.charAt(i-1) == s2.charAt(j-1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = min( + dp[i - 1][j] + 1, + dp[i][j - 1] + 1, + dp[i - 1][j - 1] + 1 + ); + } + } + } + // 储存着整个 s1 和 s2 的最小编辑距离 + return dp[m][n]; + } + + int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } } ``` -### 三、扩展延伸 + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +## 四、扩展延伸 一般来说,处理两个字符串的动态规划问题,都是按本文的思路处理,建立 DP table。为什么呢,因为易于找出状态转移的关系,比如编辑距离的 DP table: -![](../pictures/editDistance/4.jpg) +![](https://labuladong.online/algo/images/editDistance/4.jpg) 还有一个细节,既然每个 `dp[i][j]` 只和它附近的三个状态有关,空间复杂度是可以压缩成 $O(min(M, N))$ 的(M,N 是两个字符串的长度)。不难,但是可解释性大大降低,读者可以自己尝试优化一下。 @@ -252,18 +380,151 @@ class Node { 我们的最终结果不是 `dp[m][n]` 吗,这里的 `val` 存着最小编辑距离,`choice` 存着最后一个操作,比如说是插入操作,那么就可以左移一格: -![](../pictures/editDistance/5.jpg) +![](https://labuladong.online/algo/images/editDistance/5.jpg) 重复此过程,可以一步步回到起点 `dp[0][0]`,形成一条路径,按这条路径上的操作进行编辑,就是最佳方案。 -![](../pictures/editDistance/6.jpg) +![](https://labuladong.online/algo/images/editDistance/6.jpg) + +应大家的要求,我把这个思路也写出来,你可以自己运行试一下: + +```java +int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + Node[][] dp = new Node[m + 1][n + 1]; + // base case + for (int i = 0; i <= m; i++) { + // s1 转化成 s2 只需要删除一个字符 + dp[i][0] = new Node(i, 2); + } + for (int j = 1; j <= n; j++) { + // s1 转化成 s2 只需要插入一个字符 + dp[0][j] = new Node(j, 1); + } + // 状态转移方程 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (s1.charAt(i-1) == s2.charAt(j-1)){ + // 如果两个字符相同,则什么都不需要做 + Node node = dp[i - 1][j - 1]; + dp[i][j] = new Node(node.val, 0); + } else { + // 否则,记录代价最小的操作 + dp[i][j] = minNode( + dp[i - 1][j], + dp[i][j - 1], + dp[i-1][j-1] + ); + // 并且将编辑距离加一 + dp[i][j].val++; + } + } + } + // 根据 dp table 反推具体操作过程并打印 + printResult(dp, s1, s2); + return dp[m][n].val; +} + +// 计算 delete, insert, replace 中代价最小的操作 +Node minNode(Node a, Node b, Node c) { + Node res = new Node(a.val, 2); + + if (res.val > b.val) { + res.val = b.val; + res.choice = 1; + } + if (res.val > c.val) { + res.val = c.val; + res.choice = 3; + } + return res; +} +``` + +最后,`printResult` 函数反推结果并把具体的操作打印出来: + +```java +void printResult(Node[][] dp, String s1, String s2) { + int rows = dp.length; + int cols = dp[0].length; + int i = rows - 1, j = cols - 1; + System.out.println("Change s1=" + s1 + " to s2=" + s2 + ":\n"); + while (i != 0 && j != 0) { + char c1 = s1.charAt(i - 1); + char c2 = s2.charAt(j - 1); + int choice = dp[i][j].choice; + System.out.print("s1[" + (i - 1) + "]:"); + switch (choice) { + case 0: + // 跳过,则两个指针同时前进 + System.out.println("skip '" + c1 + "'"); + i--; j--; + break; + case 1: + // 将 s2[j] 插入 s1[i],则 s2 指针前进 + System.out.println("insert '" + c2 + "'"); + j--; + break; + case 2: + // 将 s1[i] 删除,则 s1 指针前进 + System.out.println("delete '" + c1 + "'"); + i--; + break; + case 3: + // 将 s1[i] 替换成 s2[j],则两个指针同时前进 + System.out.println( + "replace '" + c1 + "'" + " with '" + c2 + "'"); + i--; j--; + break; + } + } + // 如果 s1 还没有走完,则剩下的都是需要删除的 + while (i > 0) { + System.out.print("s1[" + (i - 1) + "]:"); + System.out.println("delete '" + s1.charAt(i - 1) + "'"); + i--; + } + // 如果 s2 还没有走完,则剩下的都是需要插入 s1 的 + while (j > 0) { + System.out.print("s1[0]:"); + System.out.println("insert '" + s2.charAt(j - 1) + "'"); + j--; + } +} +``` + + + +
+
+引用本文的文章 + + - [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + - [经典动态规划:正则表达式](https://labuladong.online/algo/dynamic-programming/regular-expression-matching/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [97. Interleaving String](https://leetcode.com/problems/interleaving-string/?show=1) | [97. 交错字符串](https://leetcode.cn/problems/interleaving-string/?show=1) | 🟠 | + +
+
+ -以上就是编辑距离算法的全部内容,如果本文对你有帮助,**欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚**~ -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:动态规划设计:最长递增子序列](../动态规划系列/动态规划设计:最长递增子序列.md) -[下一篇:经典动态规划问题:高楼扔鸡蛋](../动态规划系列/高楼扔鸡蛋问题.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\203\214\345\214\205\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\203\214\345\214\205\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..faff96106c --- /dev/null +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\203\214\345\214\205\351\227\256\351\242\230.md" @@ -0,0 +1,225 @@ +# 经典动态规划:0-1 背包问题 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +> tip:本文有视频版:[0-1背包问题详解](https://www.bilibili.com/video/BV15B4y1P7X7/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +后台天天有人问背包问题,这个问题其实不难,借助动态规划的思维框架,无非还是状态 + 选择,没啥特别之处。今天就来说一下背包问题吧,就讨论最常见的 0-1 背包问题。描述: + +给你一个可装载重量为 `W` 的背包和 `N` 个物品,每个物品有重量和价值两个属性。其中第 `i` 个物品的重量为 `wt[i]`,价值为 `val[i]`。现在让你用这个背包装物品,每个物品只能用一次,在不超过背包容量的前提下,最多能装的价值是多少? + +![](https://labuladong.online/algo/images/knapsack/1.png) + +举个简单的例子,输入如下: + +```py +N = 3, W = 4 +wt = [2, 1, 3] +val = [4, 2, 3] +``` + +算法返回 6,选择前两件物品装进背包,总重量 3 小于 `W`,可以获得最大价值 6。 + +题目就是这么简单,一个典型的动态规划问题。这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。 + +解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,根据我们 [动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 中的套路,直接走流程就行了。 + + + + + + + +## 动规标准套路 + +看来每篇动态规划文章都得重复一遍套路,历史文章中的动态规划问题都是按照下面的套路来的。 + +**第一步要明确两点,「状态」和「选择」**。 + +先说状态,如何才能描述一个问题局面?只要给几个物品和一个背包的容量限制,就形成了一个背包问题呀。**所以状态有两个,就是「背包的容量」和「可选择的物品」**。 + +再说选择,也很容易想到啊,对于每件物品,你能选择什么?**选择就是「装进背包」或者「不装进背包」嘛**。 + +明白了状态和选择,动态规划问题基本上就解决了,对于自底向上的思考方式,代码的一般框架是这样: + +```python +for 状态1 in 状态1的所有取值: + for 状态2 in 状态2的所有取值: + for ... + dp[状态1][状态2][...] = 择优(选择1,选择2...) +``` + +**第二步要明确 `dp` 数组的定义**。 + +首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 `dp` 数组。 + +`dp[i][w]` 的定义如下:对于前 `i` 个物品,当前背包的容量为 `w`,这种情况下可以装的最大价值是 `dp[i][w]`。 + +比如说,如果 `dp[3][5] = 6`,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。 + + + + + + + +> [!NOTE] +> 为什么要这么定义?因为这样可以找到状态转移关系,或者说这就是背包问题的特殊定义方式,你当做套路记下来就行,未来遇到动态规划相关问题,都可以这样定义试一试。 + +根据这个定义,我们想求的最终答案就是 `dp[N][W]`。base case 就是 `dp[0][..] = dp[..][0] = 0`,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。 + +细化上面的框架: + +```python +int[][] dp[N+1][W+1] +dp[0][..] = 0 +dp[..][0] = 0 + +for i in [1..N]: + for w in [1..W]: + dp[i][w] = max( + 把物品 i 装进背包, + 不把物品 i 装进背包 + ) +return dp[N][W] +``` + +**第三步,根据「选择」,思考状态转移的逻辑**。 + +简单说就是,上面伪码中「把物品 `i` 装进背包」和「不把物品 `i` 装进背包」怎么用代码体现出来呢? + +这就要结合对 `dp` 数组的定义,看看这两种选择会对状态产生什么影响: + + + + + + + +先重申一下刚才我们的 `dp` 数组的定义: + +`dp[i][w]` 表示:对于前 `i` 个物品(从 1 开始计数),当前背包的容量为 `w` 时,这种情况下可以装下的最大价值是 `dp[i][w]`。 + +**如果你没有把这第 `i` 个物品装入背包**,那么很显然,最大价值 `dp[i][w]` 应该等于 `dp[i-1][w]`,继承之前的结果。 + +**如果你把这第 `i` 个物品装入了背包**,那么 `dp[i][w]` 应该等于 `val[i-1] + dp[i-1][w - wt[i-1]]`。 + +首先,由于数组索引从 0 开始,而我们定义中的 `i` 是从 1 开始计数的,所以 `val[i-1]` 和 `wt[i-1]` 表示第 `i` 个物品的价值和重量。 + +你如果选择将第 `i` 个物品装进背包,那么第 `i` 个物品的价值 `val[i-1]` 肯定就到手了,接下来你就要在剩余容量 `w - wt[i-1]` 的限制下,在前 `i - 1` 个物品中挑选,求最大价值,即 `dp[i-1][w - wt[i-1]]`。 + +综上就是两种选择,我们都已经分析完毕,也就是写出来了状态转移方程,可以进一步细化代码: + +```python +for i in [1..N]: + for w in [1..W]: + dp[i][w] = max( + dp[i-1][w], + dp[i-1][w - wt[i-1]] + val[i-1] + ) +return dp[N][W] +``` + +**最后一步,把伪码翻译成代码,处理一些边界情况**。 + +我用 Java 写的代码,把上面的思路完全翻译了一遍,并且处理了 `w - wt[i-1]` 可能小于 0 导致数组索引越界的问题: + +```java +int knapsack(int W, int N, int[] wt, int[] val) { + assert N == wt.length; + // base case 已初始化 + int[][] dp = new int[N + 1][W + 1]; + for (int i = 1; i <= N; i++) { + for (int w = 1; w <= W; w++) { + if (w - wt[i - 1] < 0) { + // 这种情况下只能选择不装入背包 + dp[i][w] = dp[i - 1][w]; + } else { + // 装入或者不装入背包,择优 + dp[i][w] = Math.max( + dp[i - 1][w - wt[i-1]] + val[i-1], + dp[i - 1][w] + ); + } + } + } + + return dp[N][W]; +} +``` + + +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +> [!NOTE] +> 其实函数签名中的物品数量 `N` 就是 `wt` 数组的长度,所以实际上这个参数 `N` 是多此一举的。但为了体现原汁原味的 0-1 背包问题,我就带上这个参数 `N` 了,你自己写的话可以省略。 + +至此,背包问题就解决了,相比而言,我觉得这是比较简单的动态规划问题,因为状态转移的推导比较自然,基本上你明确了 `dp` 数组的定义,就可以理所当然地确定状态转移了。 + + + + + + + +
+
+引用本文的文章 + + - [扫描线技巧:安排会议室](https://labuladong.online/algo/frequency-interview/scan-line-technique/) + - [经典动态规划:子集背包问题](https://labuladong.online/algo/dynamic-programming/knapsack2/) + - [经典动态规划:完全背包问题](https://labuladong.online/algo/dynamic-programming/knapsack3/) + - [背包问题的变体:目标和](https://labuladong.online/algo/dynamic-programming/target-sum/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1235. Maximum Profit in Job Scheduling](https://leetcode.com/problems/maximum-profit-in-job-scheduling/?show=1) | [1235. 规划兼职工作](https://leetcode.cn/problems/maximum-profit-in-job-scheduling/?show=1) | 🔴 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" index 889cd26cd4..f03859a7c4 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" @@ -1,5 +1,24 @@ # 贪心算法之区间调度问题 + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [435. Non-overlapping Intervals](https://leetcode.com/problems/non-overlapping-intervals/) | [435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/) | 🟠 | +| [452. Minimum Number of Arrows to Burst Balloons](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/) | [452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) | 🟠 | + +**-----------** + + + 什么是贪心算法呢?贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。 比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到多项式级别的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到线性级别的。 @@ -8,21 +27,29 @@ 比如你面前放着 100 张人民币,你只能拿十张,怎么才能拿最多的面额?显然每次选择剩下钞票中面值最大的一张,最后你的选择一定是最优的。 -然而,大部分问题明显不具有贪心选择性质。比如打斗地主,对手出对儿三,按照贪心策略,你应该出尽可能小的牌刚好压制住对方,但现实情况我们甚至可能会出王炸。这种情况就不能用贪心算法,而得使用动态规划解决,参见前文「动态规划解决博弈问题」。 +然而,大部分问题明显不具有贪心选择性质。比如打斗地主,对手出对儿三,按照贪心策略,你应该出尽可能小的牌刚好压制住对方,但现实情况我们甚至可能会出王炸。这种情况就不能用贪心算法,而得使用动态规划解决,参见前文 [动态规划解决博弈问题](https://labuladong.online/algo/dynamic-programming/game-theory/)。 + +## 一、问题概述 -### 一、问题概述 +言归正传,本文解决一个很经典的贪心算法问题 Interval Scheduling(区间调度问题),也就是力扣第 435 题「无重叠区间」: -言归正传,本文解决一个很经典的贪心算法问题 Interval Scheduling(区间调度问题)。给你很多形如 `[start, end]` 的闭区间,请你设计一个算法,**算出这些区间中最多有几个互不相交的区间**。 +给你很多形如 `[start, end]` 的闭区间,请你设计一个算法,**算出这些区间中最多有几个互不相交的区间**。 ```java -int intervalSchedule(int[][] intvs) {} +int intervalSchedule(int[][] intvs); ``` 举个例子,`intvs = [[1,3], [2,4], [3,6]]`,这些区间最多有 2 个区间互不相交,即 `[[1,3], [3,6]]`,你的算法应该返回 2。注意边界相同并不算相交。 -这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 `[start, end]` 表示开始和结束的时间,请问你今天**最多能参加几个活动呢?**显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。 +这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 `[start, end]` 表示开始和结束的时间,请问你今天**最多能参加几个活动呢**?显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。 -### 二、贪心解法 + + + + + + +## 二、贪心解法 这个问题有许多看起来不错的贪心思路,却都不能得到正确答案。比如说: @@ -30,100 +57,163 @@ int intervalSchedule(int[][] intvs) {} 正确的思路其实很简单,可以分为以下三步: -1. 从区间集合 intvs 中选择一个区间 x,这个 x 是在当前所有区间中**结束最早的**(end 最小)。 -2. 把所有与 x 区间相交的区间从区间集合 intvs 中删除。 -3. 重复步骤 1 和 2,直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。 +1、从区间集合 `intvs` 中选择一个区间 `x`,这个 `x` 是在当前所有区间中**结束最早的**(`end` 最小)。 -把这个思路实现成算法的话,可以按每个区间的 `end` 数值升序排序,因为这样处理之后实现步骤 1 和步骤 2 都方便很多: +2、把所有与 `x` 区间相交的区间从区间集合 `intvs` 中删除。 -![1](../pictures/interval/1.gif) +3、重复步骤 1 和 2,直到 `intvs` 为空为止。之前选出的那些 `x` 就是最大不相交子集。 -现在来实现算法,对于步骤 1,由于我们预先按照 `end` 排了序,所以选择 x 是很容易的。关键在于,如何去除与 x 相交的区间,选择下一轮循环的 x 呢? +把这个思路实现成算法的话,可以按每个区间的 `end` 数值升序排序,因为这样处理之后实现步骤 1 和步骤 2 都方便很多,如下 GIF 所示: -**由于我们事先排了序**,不难发现所有与 x 相交的区间必然会与 x 的 `end` 相交;如果一个区间不想与 x 的 `end` 相交,它的 `start` 必须要大于(或等于)x 的 `end`: +![](https://labuladong.online/algo/images/interval/1.gif) -![2](../pictures/interval/2.jpg) +现在来实现算法,对于步骤 1,由于我们预先按照 `end` 排了序,所以选择 `x` 是很容易的。关键在于,如何去除与 `x` 相交的区间,选择下一轮循环的 `x` 呢? + +**由于我们事先排了序**,不难发现所有与 `x` 相交的区间必然会与 `x` 的 `end` 相交;如果一个区间不想与 `x` 的 `end` 相交,它的 `start` 必须要大于(或等于)`x` 的 `end`: + +![](https://labuladong.online/algo/images/interval/2.jpg) 看下代码: ```java -public int intervalSchedule(int[][] intvs) { - if (intvs.length == 0) return 0; - // 按 end 升序排序 - Arrays.sort(intvs, new Comparator() { - public int compare(int[] a, int[] b) { - return a[1] - b[1]; - } - }); - // 至少有一个区间不相交 - int count = 1; - // 排序后,第一个区间就是 x - int x_end = intvs[0][1]; - for (int[] interval : intvs) { - int start = interval[0]; - if (start >= x_end) { - // 找到下一个选择的区间了 - count++; - x_end = interval[1]; +class Solution { + public int intervalSchedule(int[][] intvs) { + if (intvs.length == 0) return 0; + // 按 end 升序排序 + Arrays.sort(intvs, (a, b) -> Integer.compare(a[1], b[1])); + // 至少有一个区间不相交 + int count = 1; + // 排序后,第一个区间就是 x + int x_end = intvs[0][1]; + for (int[] interval : intvs) { + int start = interval[0]; + if (start >= x_end) { + // 找到下一个选择的区间了 + count++; + x_end = interval[1]; + } } + return count; } - return count; } ``` -### 三、应用举例 +## 三、应用举例 + +下面再举例几道具体的题目应用一下区间调度算法。 + +首先是力扣第 435 题「无重叠区间」问题: + +输入一个区间的集合,请你计算,要想使其中的区间都互不重叠,至少需要移除几个区间?函数签名如下: -下面举例几道 LeetCode 题目应用一下区间调度算法。 +```java +int eraseOverlapIntervals(int[][] intvs); +``` -第 435 题,无重叠区间: +其中,可以假设输入的区间的终点总是大于起点,另外边界相等的区间只算接触,但并不算相互重叠。 -![title1](../pictures/interval/title1.png) +比如说输入是 `intvs = [[1,2],[2,3],[3,4],[1,3]]`,算法返回 1,因为只要移除 `[1,3]` 后,剩下的区间就没有重叠了。 我们已经会求最多有几个区间不会重叠了,那么剩下的不就是至少需要去除的区间吗? ```java -int eraseOverlapIntervals(int[][] intervals) { - int n = intervals.length; - return n - intervalSchedule(intervals); +class Solution { + public int eraseOverlapIntervals(int[][] intvs) { + int n = intvs.length; + return n - intervalSchedule(intvs); + } + + private int intervalSchedule(int[][] intvs) { + // 见上文 + } } ``` -第 452 题,用最少的箭头射爆气球: -![title2](../pictures/interval/title2.png) +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +再说说力扣第 452 题「用最少的箭头射爆气球」,我来描述一下题目: + +假设在二维平面上有很多圆形的气球,这些圆形投影到 x 轴上会形成一个个区间对吧。那么给你输入这些区间,你沿着 x 轴前进,可以垂直向上射箭,请问你至少要射几箭才能把这些气球全部射爆呢? + +函数签名如下: + +```java +int findMinArrowShots(int[][] intvs); +``` + +比如说输入为 `[[10,16],[2,8],[1,6],[7,12]]`,算法应该返回 2,因为我们可以在 `x` 为 6 的地方射一箭,射爆 `[2,8]` 和 `[1,6]` 两个气球,然后在 `x` 为 10,11 或 12 的地方射一箭,射爆 `[10,16]` 和 `[7,12]` 两个气球。 其实稍微思考一下,这个问题和区间调度算法一模一样!如果最多有 `n` 个不重叠的区间,那么就至少需要 `n` 个箭头穿透所有区间: -![3](../pictures/interval/3.jpg) +![](https://labuladong.online/algo/images/interval/3.jpg) 只是有一点不一样,在 `intervalSchedule` 算法中,如果两个区间的边界触碰,不算重叠;而按照这道题目的描述,箭头如果碰到气球的边界气球也会爆炸,所以说相当于区间的边界触碰也算重叠: -![4](../pictures/interval/4.jpg) +![](https://labuladong.online/algo/images/interval/4.jpg) 所以只要将之前的算法稍作修改,就是这道题目的答案: ```java -int findMinArrowShots(int[][] intvs) { - // ... - - for (int[] interval : intvs) { - int start = interval[0]; - // 把 >= 改成 > 就行了 - if (start > x_end) { - count++; - x_end = interval[1]; +class Solution { + public int findMinArrowShots(int[][] intvs) { + if (intvs.length == 0) return 0; + Arrays.sort(intvs, (a, b) -> Integer.compare(a[1], b[1])); + int count = 1; + int x_end = intvs[0][1]; + + for (int[] interval : intvs) { + int start = interval[0]; + // 把 >= 改成 > 就行了 + if (start > x_end) { + count++; + x_end = interval[1]; + } } + return count; } - return count; } ``` -这么做的原因也不难理解,因为现在边界接触也算重叠,所以 `start == x_end` 时不能更新 x。 -如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~ +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + +
+
+引用本文的文章 + + - [一个方法解决三道区间问题](https://labuladong.online/algo/practice-in-action/interval-problem-summary/) + - [剪视频剪出一个贪心算法](https://labuladong.online/algo/frequency-interview/cut-video/) + - [扫描线技巧:安排会议室](https://labuladong.online/algo/frequency-interview/scan-line-technique/) + +

+ + + + + +**_____________** -[上一篇:动态规划之博弈问题](../动态规划系列/动态规划之博弈问题.md) -[下一篇:动态规划之KMP字符匹配算法](../动态规划系列/动态规划之KMP字符匹配算法.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" deleted file mode 100644 index 50ff9efecc..0000000000 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" +++ /dev/null @@ -1,275 +0,0 @@ -# 经典动态规划问题:高楼扔鸡蛋(进阶) - -上篇文章聊了高楼扔鸡蛋问题,讲了一种效率不是很高,但是较为容易理解的动态规划解法。后台很多读者问如何更高效地解决这个问题,今天就谈两种思路,来优化一下这个问题,分别是二分查找优化和重新定义状态转移。 - -如果还不知道高楼扔鸡蛋问题的读者可以看下「经典动态规划:高楼扔鸡蛋」,那篇文章详解了题目的含义和基本的动态规划解题思路,请确保理解前文,因为今天的优化都是基于这个基本解法的。 - -二分搜索的优化思路也许是我们可以尽力尝试写出的,而修改状态转移的解法可能是不容易想到的,可以借此见识一下动态规划算法设计的玄妙,当做思维拓展。 - -### 二分搜索优化 - -之前提到过这个解法,核心是因为状态转移方程的单调性,这里可以具体展开看看。 - -首先简述一下原始动态规划的思路: - -1、暴力穷举尝试在所有楼层 `1 <= i <= N` 扔鸡蛋,每次选择尝试次数**最少**的那一层; - -2、每次扔鸡蛋有两种可能,要么碎,要么没碎; - -3、如果鸡蛋碎了,`F` 应该在第 `i` 层下面,否则,`F` 应该在第 `i` 层上面; - -4、鸡蛋是碎了还是没碎,取决于哪种情况下尝试次数**更多**,因为我们想求的是最坏情况下的结果。 - -核心的状态转移代码是这段: - -```python -# 当前状态为 K 个鸡蛋,面对 N 层楼 -# 返回这个状态下的最优结果 -def dp(K, N): - for 1 <= i <= N: - # 最坏情况下的最少扔鸡蛋次数 - res = min(res, - max( - dp(K - 1, i - 1), # 碎 - dp(K, N - i) # 没碎 - ) + 1 # 在第 i 楼扔了一次 - ) - return res -``` - -这个 for 循环就是下面这个状态转移方程的具体代码实现: - -$$ dp(K, N) = \min_{0 <= i <= N}\{\max\{dp(K - 1, i - 1), dp(K, N - i)\} + 1\}$$ - -如果能够理解这个状态转移方程,那么就很容易理解二分查找的优化思路。 - -首先我们根据 `dp(K, N)` 数组的定义(有 `K` 个鸡蛋面对 `N` 层楼,最少需要扔几次),**很容易知道 `K` 固定时,这个函数随着 `N` 的增加一定是单调递增的**,无论你策略多聪明,楼层增加测试次数一定要增加。 - -那么注意 `dp(K - 1, i - 1)` 和 `dp(K, N - i)` 这两个函数,其中 `i` 是从 1 到 `N` 单增的,如果我们固定 `K` 和 `N`,**把这两个函数看做关于 `i` 的函数,前者随着 `i` 的增加应该也是单调递增的,而后者随着 `i` 的增加应该是单调递减的**: - -![](../pictures/扔鸡蛋/2.jpg) - -这时候求二者的较大值,再求这些最大值之中的最小值,其实就是求这两条直线交点,也就是红色折线的最低点嘛。 - -我们前文「二分查找只能用来查找元素吗」讲过,二分查找的运用很广泛,形如下面这种形式的 for 循环代码: - -```java -for (int i = 0; i < n; i++) { - if (isOK(i)) - return i; -} -``` - -都很有可能可以运用二分查找来优化线性搜索的复杂度,回顾这两个 `dp` 函数的曲线,我们要找的最低点其实就是这种情况: - -```java -for (int i = 1; i <= N; i++) { - if (dp(K - 1, i - 1) == dp(K, N - i)) - return dp(K, N - i); -} -``` - -熟悉二分搜索的同学肯定敏感地想到了,这不就是相当于求 Valley(山谷)值嘛,可以用二分查找来快速寻找这个点的,直接看代码吧,整体的思路还是一样,只是加快了搜索速度: - -```python -def superEggDrop(self, K: int, N: int) -> int: - - memo = dict() - def dp(K, N): - if K == 1: return N - if N == 0: return 0 - if (K, N) in memo: - return memo[(K, N)] - - # for 1 <= i <= N: - # res = min(res, - # max( - # dp(K - 1, i - 1), - # dp(K, N - i) - # ) + 1 - # ) - - res = float('INF') - # 用二分搜索代替线性搜索 - lo, hi = 1, N - while lo <= hi: - mid = (lo + hi) // 2 - broken = dp(K - 1, mid - 1) # 碎 - not_broken = dp(K, N - mid) # 没碎 - # res = min(max(碎,没碎) + 1) - if broken > not_broken: - hi = mid - 1 - res = min(res, broken + 1) - else: - lo = mid + 1 - res = min(res, not_broken + 1) - - memo[(K, N)] = res - return res - - return dp(K, N) -``` - -这个算法的时间复杂度是多少呢?**动态规划算法的时间复杂度就是子问题个数 × 函数本身的复杂度**。 - -函数本身的复杂度就是忽略递归部分的复杂度,这里 `dp` 函数中用了一个二分搜索,所以函数本身的复杂度是 O(logN)。 - -子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。 - -所以算法的总时间复杂度是 O(K\*N\*logN), 空间复杂度 O(KN)。效率上比之前的算法 O(KN^2) 要高效一些。 - -### 重新定义状态转移 - -前文「不同定义有不同解法」就提过,找动态规划的状态转移本就是见仁见智,比较玄学的事情,不同的状态定义可以衍生出不同的解法,其解法和复杂程度都可能有巨大差异。这里就是一个很好的例子。 - -再回顾一下我们之前定义的 `dp` 数组含义: - -```python -def dp(k, n) -> int -# 当前状态为 k 个鸡蛋,面对 n 层楼 -# 返回这个状态下最少的扔鸡蛋次数 -``` - -用 dp 数组表示的话也是一样的: - -```python -dp[k][n] = m -# 当前状态为 k 个鸡蛋,面对 n 层楼 -# 这个状态下最少的扔鸡蛋次数为 m -``` - -按照这个定义,就是**确定当前的鸡蛋个数和面对的楼层数,就知道最小扔鸡蛋次数**。最终我们想要的答案就是 `dp(K, N)` 的结果。 - -这种思路下,肯定要穷举所有可能的扔法的,用二分搜索优化也只是做了「剪枝」,减小了搜索空间,但本质思路没有变,还是穷举。 - -现在,我们稍微修改 `dp` 数组的定义,**确定当前的鸡蛋个数和最多允许的扔鸡蛋次数,就知道能够确定 `F` 的最高楼层数**。具体来说是这个意思: - -```python -dp[k][m] = n -# 当前有 k 个鸡蛋,可以尝试扔 m 次鸡蛋 -# 这个状态下,最坏情况下最多能确切测试一栋 n 层的楼 - -# 比如说 dp[1][7] = 7 表示: -# 现在有 1 个鸡蛋,允许你扔 7 次; -# 这个状态下最多给你 7 层楼, -# 使得你可以确定楼层 F 使得鸡蛋恰好摔不碎 -# (一层一层线性探查嘛) -``` - -这其实就是我们原始思路的一个「反向」版本,我们先不管这种思路的状态转移怎么写,先来思考一下这种定义之下,最终想求的答案是什么? - -我们最终要求的其实是扔鸡蛋次数 `m`,但是这时候 `m` 在状态之中而不是 `dp` 数组的结果,可以这样处理: - -```java -int superEggDrop(int K, int N) { - - int m = 0; - while (dp[K][m] < N) { - m++; - // 状态转移... - } - return m; -} -``` - -题目不是**给你 `K` 鸡蛋,`N` 层楼,让你求最坏情况下最少的测试次数 `m`** 吗?`while` 循环结束的条件是 `dp[K][m] == N`,也就是**给你 `K` 个鸡蛋,测试 `m` 次,最坏情况下最多能测试 `N` 层楼**。 - -注意看这两段描述,是完全一样的!所以说这样组织代码是正确的,关键就是状态转移方程怎么找呢?还得从我们原始的思路开始讲。之前的解法配了这样图帮助大家理解状态转移思路: - -![](../pictures/扔鸡蛋/1.jpg) - -这个图描述的仅仅是某一个楼层 `i`,原始解法还得线性或者二分扫描所有楼层,要求最大值、最小值。但是现在这种 `dp` 定义根本不需要这些了,基于下面两个事实: - -**1、无论你在哪层楼扔鸡蛋,鸡蛋只可能摔碎或者没摔碎,碎了的话就测楼下,没碎的话就测楼上**。 - -**2、无论你上楼还是下楼,总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)**。 - -根据这个特点,可以写出下面的状态转移方程: - -`dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1` - -**`dp[k][m - 1]` 就是楼上的楼层数**,因为鸡蛋个数 `k` 不变,也就是鸡蛋没碎,扔鸡蛋次数 `m` 减一; - -**`dp[k - 1][m - 1]` 就是楼下的楼层数**,因为鸡蛋个数 `k` 减一,也就是鸡蛋碎了,同时扔鸡蛋次数 `m` 减一。 - -PS:这个 `m` 为什么要减一而不是加一?之前定义得很清楚,这个 `m` 是一个允许的次数上界,而不是扔了几次。 - -![](../pictures/扔鸡蛋/3.jpg) - -至此,整个思路就完成了,只要把状态转移方程填进框架即可: - -```java -int superEggDrop(int K, int N) { - // m 最多不会超过 N 次(线性扫描) - int[][] dp = new int[K + 1][N + 1]; - // base case: - // dp[0][..] = 0 - // dp[..][0] = 0 - // Java 默认初始化数组都为 0 - int m = 0; - while (dp[K][m] < N) { - m++; - for (int k = 1; k <= K; k++) - dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; - } - return m; -} -``` - -如果你还觉得这段代码有点难以理解,其实它就等同于这样写: - -```java -for (int m = 1; dp[K][m] < N; m++) - for (int k = 1; k <= K; k++) - dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; -``` - -看到这种代码形式就熟悉多了吧,因为我们要求的不是 `dp` 数组里的值,而是某个符合条件的索引 `m`,所以用 `while` 循环来找到这个 `m` 而已。 - -这个算法的时间复杂度是多少?很明显就是两个嵌套循环的复杂度 O(KN)。 - -另外注意到 `dp[m][k]` 转移只和左边和左上的两个状态有关,所以很容易优化成一维 `dp` 数组,这里就不写了。 - -### 还可以再优化 - -再往下就要用一些数学方法了,不具体展开,就简单提一下思路吧。 - -在刚才的思路之上,**注意函数 `dp(m, k)` 是随着 `m` 单增的,因为鸡蛋个数 `k` 不变时,允许的测试次数越多,可测试的楼层就越高**。 - -这里又可以借助二分搜索算法快速逼近 `dp[K][m] == N` 这个终止条件,时间复杂度进一步下降为 O(KlogN),我们可以设 `g(k, m) =`…… - -算了算了,打住吧。我觉得我们能够写出 O(K\*N\*logN) 的二分优化算法就行了,后面的这些解法呢,听个响鼓个掌就行了,把欲望限制在能力的范围之内才能拥有快乐! - -不过可以肯定的是,根据二分搜索代替线性扫描 `m` 的取值,代码的大致框架肯定是修改穷举 `m` 的 for 循环: - -```java -// 把线性搜索改成二分搜索 -// for (int m = 1; dp[K][m] < N; m++) -int lo = 1, hi = N; -while (lo < hi) { - int mid = (lo + hi) / 2; - if (... < N) { - lo = ... - } else { - hi = ... - } - - for (int k = 1; k <= K; k++) - // 状态转移方程 -} -``` - -简单总结一下吧,第一个二分优化是利用了 `dp` 函数的单调性,用二分查找技巧快速搜索答案;第二种优化是巧妙地修改了状态转移方程,简化了求解了流程,但相应的,解题逻辑比较难以想到;后续还可以用一些数学方法和二分搜索进一步优化第二种解法,不过看了看镜子中的发量,算了。 - -本文终,希望对你有一点启发。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:经典动态规划问题:高楼扔鸡蛋](../动态规划系列/高楼扔鸡蛋问题.md) - -[下一篇:动态规划之子序列问题解题模板](../动态规划系列/子序列问题模板.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" index 6fc72e09e6..56fa24f9ec 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" @@ -1,14 +1,45 @@ -# 经典动态规划问题:高楼扔鸡蛋 +# 经典动态规划:高楼扔鸡蛋 -今天要聊一个很经典的算法问题,若干层楼,若干个鸡蛋,让你算出最少的尝试次数,找到鸡蛋恰好摔不碎的那层楼。国内大厂以及谷歌脸书面试都经常考察这道题,只不过他们觉得扔鸡蛋太浪费,改成扔杯子,扔破碗什么的。 -具体的问题等会再说,但是这道题的解法技巧很多,光动态规划就好几种效率不同的思路,最后还有一种极其高效数学解法。秉承咱们号一贯的作风,拒绝奇技淫巧,拒绝过于诡异的技巧,因为这些技巧无法举一反三,学了也不划算。 + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [887. Super Egg Drop](https://leetcode.com/problems/super-egg-drop/) | [887. 鸡蛋掉落](https://leetcode.cn/problems/super-egg-drop/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +本文要聊一个很经典的算法问题,若干层楼,若干个鸡蛋,让你算出最少的尝试次数,找到鸡蛋恰好摔不碎的那层楼。国内大厂以及谷歌脸书面试都经常考察这道题,只不过他们觉得扔鸡蛋太浪费,改成扔杯子,扔破碗什么的。 + +具体的问题等会再说,但是这道题的解法技巧很多,光动态规划就好几种效率不同的思路,最后还有一种极其高效数学解法。秉承本书一贯的作风,拒绝过于诡异的技巧,因为这些技巧无法举一反三,学了也不划算。 下面就来用我们一直强调的动态规划通用思路来研究一下这道题。 -### 一、解析题目 -题目是这样:你面前有一栋从 1 到 `N` 共 `N` 层的楼,然后给你 `K` 个鸡蛋(`K` 至少为 1)。现在确定这栋楼存在楼层 `0 <= F <= N`,在这层楼将鸡蛋扔下去,鸡蛋**恰好没摔碎**(高于 `F` 的楼层都会碎,低于 `F` 的楼层都不会碎)。现在问你,**最坏**情况下,你**至少**要扔几次鸡蛋,才能**确定**这个楼层 `F` 呢? + + + + + +## 一、解析题目 + +这是力扣第 887 题「鸡蛋掉落」,我描述一下题目: + +你面前有一栋从 1 到 `N` 共 `N` 层的楼,然后给你 `K` 个鸡蛋(`K` 至少为 1)。现在确定这栋楼存在楼层 `0 <= F <= N`,在这层楼将鸡蛋扔下去,鸡蛋**恰好没摔碎**(高于 `F` 的楼层都会碎,低于 `F` 的楼层都不会碎,如果鸡蛋没有碎,可以捡回来继续扔)。现在问你,**最坏**情况下,你**至少**要扔几次鸡蛋,才能**确定**这个楼层 `F` 呢? 也就是让你找摔不碎鸡蛋的最高楼层 `F`,但什么叫「最坏情况」下「至少」要扔几次呢?我们分别举个例子就明白了。 @@ -30,23 +61,33 @@ 以这种策略,**最坏**情况应该是试到第 7 层鸡蛋还没碎(`F = 7`),或者鸡蛋一直碎到第 1 层(`F = 0`)。然而无论那种最坏情况,只需要试 `log7` 向上取整等于 3 次,比刚才尝试 7 次要少,这就是所谓的**至少**要扔几次。 -PS:这有点像 Big O 表示法计算​算法的复杂度。 + + + + + 实际上,如果不限制鸡蛋个数的话,二分思路显然可以得到最少尝试的次数,但问题是,**现在给你了鸡蛋个数的限制 `K`,直接使用二分思路就不行了**。 -比如说只给你 1 个鸡蛋,7 层楼,你敢用二分吗?你直接去第 4 层扔一下,如果鸡蛋没碎还好,但如果碎了你就没有鸡蛋继续测试了,无法确定鸡蛋恰好摔不碎的楼层 `F` 了。这种情况下只能用线性扫描的方法,算法返回结果应该是 7。 +比如说只给你 1 个鸡蛋,7 层楼,你敢用二分吗?你直接去第 4 层扔一下,如果鸡蛋没碎还好,你可以把鸡蛋捡起来再去更高的楼层尝试;但如果碎了,你就没有鸡蛋继续测试了,无法确定鸡蛋恰好摔不碎的楼层 `F` 了。 + +其实这种情况下只能用线性扫描的方法,从下往上一层层尝试扔鸡蛋,那么最坏情况下需要扔 7 次,算法返回结果应该是 7。 有的读者也许会有这种想法:二分查找排除楼层的速度无疑是最快的,那干脆先用二分查找,等到只剩 1 个鸡蛋的时候再执行线性扫描,这样得到的结果是不是就是最少的扔鸡蛋次数呢? 很遗憾,并不是,比如说把楼层变高一些,100 层,给你 2 个鸡蛋,你在 50 层扔一下,碎了,那就只能线性扫描 1~49 层了,最坏情况下要扔 50 次。 -如果不要「二分」,变成「五分」「十分」都会大幅减少最坏情况下的尝试次数。比方说第一个鸡蛋每隔十层楼扔,在哪里碎了第二个鸡蛋一个个线性扫描,总共不会超过 20 次​。 - -最优解其实是 14 次。最优策略非常多,而且并没有什么规律可言。 +如果不要「二分」,变成「五分」「十分」都会大幅减少最坏情况下的尝试次数。比方说第一个鸡蛋每隔十层楼扔,在哪里碎了第二个鸡蛋一个个线性扫描,总共不会超过 20 次。最优解其实是 14 次。最优策略非常多,而且并没有什么规律可言。 说了这么多废话,就是确保大家理解了题目的意思,而且认识到这个题目确实复杂,就连我们手算都不容易,如何用算法解决呢? -### 二、思路分析 + + + + + + +## 二、思路分析 对动态规划问题,直接套我们以前多次强调的框架即可:这个问题有什么「状态」,有什么「选择」,然后穷举。 @@ -55,17 +96,25 @@ PS:这有点像 Big O 表示法计算​算法的复杂度。 **「选择」其实就是去选择哪层楼扔鸡蛋**。回顾刚才的线性扫描和二分思路,二分查找每次选择到楼层区间的中间去扔鸡蛋,而线性扫描选择一层层向上测试。不同的选择会造成状态的转移。 现在明确了「状态」和「选择」,**动态规划的基本思路就形成了**:肯定是个二维的 `dp` 数组或者带有两个状态参数的 `dp` 函数来表示状态转移;外加一个 for 循环来遍历所有选择,择最优的选择更新状态: - -```python -# 当前状态为 K 个鸡蛋,面对 N 层楼 -# 返回这个状态下的最优结果 -def dp(K, N): - int res - for 1 <= i <= N: - res = min(res, 这次在第 i 层楼扔鸡蛋) - return res + + + + + +```java +// 定义:当前状态为 K 个鸡蛋,面对 N 层楼 +// 返回这个状态下最少的扔鸡蛋次数 +int dp(int K, int N) { + int res; + for (int i = 1; i <= N; i++) { + res = Math.min(res, 这次在第 i 层楼扔鸡蛋); + } + return res; +} ``` + + 这段伪码还没有展示递归和状态转移,不过大致的算法框架已经完成了。 我们选择在第 `i` 层楼扔了鸡蛋之后,可能出现两种情况:鸡蛋碎了,鸡蛋没碎。**注意,这时候状态转移就来了**: @@ -74,164 +123,431 @@ def dp(K, N): **如果鸡蛋没碎**,那么鸡蛋的个数 `K` 不变,搜索的楼层区间应该从 `[1..N]` 变为 `[i+1..N]` 共 `N-i` 层楼。 -![](../pictures/扔鸡蛋/1.jpg) +![](https://labuladong.online/algo/images/drop-egg/1.jpg) -PS:细心的读者可能会问,在第i层楼扔鸡蛋如果没碎,楼层的搜索区间缩小至上面的楼层,是不是应该包含第i层楼呀?不必,因为已经包含了。开头说了 F 是可以等于 0 的,向上递归后,第i层楼其实就相当于第 0 层,可以被取到,所以说并没有错误。 +> [!NOTE] +> 细心的读者可能会问,在第 `i` 层楼扔鸡蛋如果没碎,楼层的搜索区间缩小至上面的楼层,是不是应该包含第 `i` 层楼呀?不必,因为已经包含了。开头说了 `F` 是可以等于 0 的,向上递归后,第 `i` 层楼其实就相当于第 0 层,可以被取到,所以说并没有错误。 因为我们要求的是**最坏情况**下扔鸡蛋的次数,所以鸡蛋在第 `i` 层楼碎没碎,取决于那种情况的结果**更大**: -```python -def dp(K, N): + + + + +```java +int dp(int K, int N): for 1 <= i <= N: - # 最坏情况下的最少扔鸡蛋次数 - res = min(res, - max( - dp(K - 1, i - 1), # 碎 - dp(K, N - i) # 没碎 - ) + 1 # 在第 i 楼扔了一次 - ) + // 最坏情况下的最少扔鸡蛋次数 + res = min(res, max( + // 碎 + dp(K - 1, i - 1), + // 没碎 + dp(K, N - i), + ) + 1 + // 在第 i 楼扔了一次,所以加一 + ) return res ``` -递归的 base case 很容易理解:当楼层数 `N` 等于 0 时,显然不需要扔鸡蛋;当鸡蛋数 `K` 为 1 时,显然只能线性扫描所有楼层: +递归的 base case 很容易理解,当楼层数 `N` 等于 0 时,显然不需要扔鸡蛋;当鸡蛋数 `K` 为 1 时,显然只能线性扫描所有楼层: -```python -def dp(K, N): - if K == 1: return N - if N == 0: return 0 - ... +```java +int dp(int K, int N) { + // base case + if (K == 1) return N; + if (N == 0) return 0; + // ... +} ``` + + 至此,其实这道题就解决了!只要添加一个备忘录消除重叠子问题即可: -```python -def superEggDrop(K: int, N: int): - - memo = dict() - def dp(K, N) -> int: - # base case - if K == 1: return N - if N == 0: return 0 - # 避免重复计算 - if (K, N) in memo: - return memo[(K, N)] - - res = float('INF') - # 穷举所有可能的选择 - for i in range(1, N + 1): - res = min(res, - max( - dp(K, N - i), - dp(K - 1, i - 1) - ) + 1 - ) - # 记入备忘录 - memo[(K, N)] = res - return res - - return dp(K, N) +```java +class Solution { + // 备忘录 + int[][] memo; + + public int superEggDrop(int K, int N) { + // m 最多不会超过 N 次(线性扫描) + memo = new int[K + 1][N + 1]; + for (int[] row : memo) { + Arrays.fill(row, -666); + } + return dp(K, N); + } + + // 定义:手握 K 个鸡蛋,面对 N 层楼,最少的扔鸡蛋次数为 dp(K, N) + int dp(int K, int N) { + // base case + if (K == 1) return N; + if (N == 0) return 0; + + // 查备忘录避免冗余计算 + if (memo[K][N] != -666) { + return memo[K][N]; + } + // 状态转移方程 + int res = Integer.MAX_VALUE; + for (int i = 1; i <= N; i++) { + // 在所有楼层进行尝试,取最少扔鸡蛋次数 + res = Math.min( + res, + // 碎和没碎取最坏情况 + Math.max(dp(K, N - i), dp(K - 1, i - 1)) + 1 + ); + } + // 结果存入备忘录 + memo[K][N] = res; + return res; + } +} ``` 这个算法的时间复杂度是多少呢?**动态规划算法的时间复杂度就是子问题个数 × 函数本身的复杂度**。 函数本身的复杂度就是忽略递归部分的复杂度,这里 `dp` 函数中有一个 for 循环,所以函数本身的复杂度是 O(N)。 -子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。 - -所以算法的总时间复杂度是 O(K*N^2), 空间复杂度 O(KN)。 - -### 三、疑难解答 +子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。所以算法的总时间复杂度是 O(K*N^2), 空间复杂度 O(KN)。 这个问题很复杂,但是算法代码却十分简洁,这就是动态规划的特性,穷举加备忘录/DP table 优化,真的没啥新意。 -首先,有读者可能不理解代码中为什么用一个 for 循环遍历楼层 `[1..N]`,也许会把这个逻辑和之前探讨的线性扫描混为一谈。其实不是的,**这只是在做一次「选择」**。 +有读者可能不理解代码中为什么用一个 for 循环遍历楼层 `[1..N]`,也许会把这个逻辑和之前探讨的线性扫描混为一谈。其实不是的,**这只是在做一次「选择」**。 -比方说你有 2 个鸡蛋,面对 10 层楼,你**这次**选择去哪一层楼扔呢?不知道,那就把这 10 层楼全试一遍。至于下次怎么选择不用你操心,有正确的状态转移,递归会算出每个选择的代价,我们取最优的那个就是最优解。 +比方说你有 2 个鸡蛋,面对 10 层楼,你**这次**选择去哪一层楼扔呢?不知道,那就把这 10 层楼全试一遍。至于下次怎么选择不用你操心,有正确的状态转移,递归算法会把每个选择的代价都算出来,我们取最优的那个就是最优解。 另外,这个问题还有更好的解法,比如修改代码中的 for 循环为二分搜索,可以将时间复杂度降为 O(K\*N\*logN);再改进动态规划解法可以进一步降为 O(KN);使用数学方法解决,时间复杂度达到最优 O(K*logN),空间复杂度达到 O(1)。 二分的解法也有点误导性,你很可能以为它跟我们之前讨论的二分思路扔鸡蛋有关系,实际上没有半毛钱关系。能用二分搜索是因为状态转移方程的函数图像具有单调性,可以快速找到最值。 -简单介绍一下二分查找的优化吧,其实只是在优化这段代码: +接下来我们看一看如何优化。 -```python -def dp(K, N): + + + + + + +## 三、二分搜索优化 + +二分搜索的优化的核心是状态转移方程的单调性,首先简述一下原始动态规划的思路: + +1、暴力穷举尝试在所有楼层 `1 <= i <= N` 扔鸡蛋,每次选择尝试次数**最少**的那一层; + +2、每次扔鸡蛋有两种可能,要么碎,要么没碎; + +3、如果鸡蛋碎了,`F` 应该在第 `i` 层下面,否则,`F` 应该在第 `i` 层上面; + +4、鸡蛋是碎了还是没碎,取决于哪种情况下尝试次数**更多**,因为我们想求的是最坏情况下的结果。 + +核心的状态转移代码是这段: + + + + + +```java +// 当前状态为 K 个鸡蛋,面对 N 层楼 +// 返回这个状态下的最优结果 +int dp(int K, int N): for 1 <= i <= N: - # 最坏情况下的最少扔鸡蛋次数 - res = min(res, - max( - dp(K - 1, i - 1), # 碎 - dp(K, N - i) # 没碎 - ) + 1 # 在第 i 楼扔了一次 - ) + // 最坏情况下的最少扔鸡蛋次数 + res = min(res, max( + // 碎 + dp(K - 1, i - 1), + // 没碎 + dp(K, N - i), + ) + 1 + // 在第 i 楼扔了一次,所以加一 + ) return res ``` + + 这个 for 循环就是下面这个状态转移方程的具体代码实现: -$$ dp(K, N) = \min_{0 <= i <= N}\{\max\{dp(K - 1, i - 1), dp(K, N - i)\} + 1\}$$ +![](https://labuladong.online/algo/images/drop-egg/formula1.png) -首先我们根据 `dp(K, N)` 数组的定义(有 `K` 个鸡蛋面对 `N` 层楼,最少需要扔几次),**很容易知道 `K` 固定时,这个函数一定是单调递增的**,无论你策略多聪明,楼层增加测试次数一定要增加。 +如果能够理解这个状态转移方程,那么就很容易理解二分查找的优化思路。 + +首先我们根据 `dp(K, N)` 数组的定义(有 `K` 个鸡蛋面对 `N` 层楼,最少需要扔几次),**很容易知道 `K` 固定时,这个函数随着 `N` 的增加一定是单调递增的**,无论你策略多聪明,楼层增加测试次数一定要增加。 那么注意 `dp(K - 1, i - 1)` 和 `dp(K, N - i)` 这两个函数,其中 `i` 是从 1 到 `N` 单增的,如果我们固定 `K` 和 `N`,**把这两个函数看做关于 `i` 的函数,前者随着 `i` 的增加应该也是单调递增的,而后者随着 `i` 的增加应该是单调递减的**: -![](../pictures/扔鸡蛋/2.jpg) +![](https://labuladong.online/algo/images/drop-egg/2.jpg) -这时候求二者的较大值,再求这些最大值之中的最小值,其实就是求这个交点嘛,熟悉二分搜索的同学肯定敏感地想到了,这不就是相当于求 Valley(山谷)值嘛,可以用二分查找来快速寻找这个点的。 +这时候求二者的较大值,再求这些最大值之中的最小值,其实就是求这两条直线交点,也就是红色折线的最低点嘛。 -直接贴一下代码吧,思路还是完全一样的: +我们前文 [二分查找的运用技巧](https://labuladong.online/algo/frequency-interview/binary-search-in-action/) 讲过,二分查找的运用很广泛,只要能够找到具有单调性的函数关系,都很有可能可以运用二分查找来优化线性搜索的复杂度。回顾这两个 `dp` 函数的曲线,我们要找的最低点其实就是这种情况: + +```java +for (int i = 1; i <= N; i++) { + if (dp(K - 1, i - 1) == dp(K, N - i)) + return dp(K, N - i); +} +``` + +熟悉二分搜索的同学肯定敏感地想到了,这不就是相当于求 Valley(山谷)值嘛,可以用二分查找来快速寻找这个点的,直接看代码吧,将 `dp` 函数的线性搜索改造成了二分搜索,加快了搜索速度: + +```java +class Solution { + // 备忘录 + int[][] memo; + + public int superEggDrop(int K, int N) { + // m 最多不会超过 N 次(线性扫描) + memo = new int[K + 1][N + 1]; + for (int[] row : memo) { + Arrays.fill(row, -666); + } + return dp(K, N); + } + + // 定义:手握 K 个鸡蛋,面对 N 层楼,最少的扔鸡蛋次数为 dp(K, N) + int dp(int K, int N) { + // base case + if (K == 1) return N; + if (N == 0) return 0; + + // 查备忘录避免冗余计算 + if (memo[K][N] != -666) { + return memo[K][N]; + } + + // for (int i = 1; i <= N; i++) { + // res = Math.min( + // res, + // Math.max(dp(K, N - i), dp(K - 1, i - 1)) + 1 + // ); + // } + + // 用二分搜索代替线性搜索 + int res = Integer.MAX_VALUE; + int lo = 1, hi = N; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + // 鸡蛋在第 mid 层碎了和没碎两种情况 + int broken = dp(K - 1, mid - 1); + int not_broken = dp(K, N - mid); + // res = min(max(碎,没碎) + 1) + if (broken > not_broken) { + hi = mid - 1; + res = Math.min(res, broken + 1); + } else { + lo = mid + 1; + res = Math.min(res, not_broken + 1); + } + } + memo[K][N] = res; + return res; + } +} +``` + +这个算法的时间复杂度是多少呢?**动态规划算法的时间复杂度就是子问题个数 × 函数本身的复杂度**。 + +函数本身的复杂度就是忽略递归部分的复杂度,这里 `dp` 函数中用了一个二分搜索,所以函数本身的复杂度是 O(logN)。 + +子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。 + +所以算法的总时间复杂度是 O(KNlogN), 空间复杂度 O(KN)。效率上比之前的算法 O(KN^2) 要高效一些。 + +## 四、重新定义状态转移 + +找动态规划的状态转移本就是见仁见智,比较玄学的事情,不同的状态定义可以衍生出不同的解法,其解法和复杂程度都可能有巨大差异,这里就是一个很好的例子。 + +再回顾一下我们之前定义的 `dp` 数组含义: + + + + + +```java +int dp(int k, int n) +// 当前状态为 k 个鸡蛋,面对 n 层楼 +// 返回这个状态下最少的扔鸡蛋次数 +``` + +用 `dp` 数组表示的话也是一样的: + +```java +dp[k][n] = m +// 当前状态为 k 个鸡蛋,面对 n 层楼 +// 这个状态下最少的扔鸡蛋次数为 m +``` + + + +按照这个定义,就是**确定当前的鸡蛋个数和面对的楼层数,就知道最小扔鸡蛋次数**。最终我们想要的答案就是 `dp(K, N)` 的结果。 + +这种思路下,肯定要穷举所有可能的扔法的,用二分搜索优化也只是做了「剪枝」,减小了搜索空间,但本质思路没有变,还是穷举。 + +现在,我们稍微修改 `dp` 数组的定义,**确定当前的鸡蛋个数和最多允许的扔鸡蛋次数,就知道能够确定 `F` 的最高楼层数**。具体来说是这个意思: + + + + + +```java +dp[k][m] = n +// 当前有 k 个鸡蛋,可以尝试扔 m 次鸡蛋 +// 这个状态下,最坏情况下最多能确切测试一栋 n 层的楼 + +// 比如说 dp[1][7] = 7 表示: +// 现在有 1 个鸡蛋,允许你扔 7 次; +// 这个状态下最多给你 7 层楼, +// 使得你可以确定楼层 F 使得鸡蛋恰好摔不碎 +// (一层一层线性探查嘛) +``` + + + +这其实就是我们原始思路的一个「反向」版本,我们先不管这种思路的状态转移怎么写,先来思考一下这种定义之下,最终想求的答案是什么? + +我们最终要求的其实是扔鸡蛋次数 `m`,但是这时候 `m` 在状态之中而不是 `dp` 数组的结果,可以这样处理: + +```java +int superEggDrop(int K, int N) { + + int m = 0; + while (dp[K][m] < N) { + m++; + // 状态转移... + } + return m; +} +``` + +题目不是**给你 `K` 鸡蛋,`N` 层楼,让你求最坏情况下最少的测试次数 `m`** 吗?`while` 循环结束的条件是 `dp[K][m] == N`,也就是**给你 `K` 个鸡蛋,测试 `m` 次,最坏情况下最多能测试 `N` 层楼**。 + +注意看这两段描述,是完全一样的!所以说这样组织代码是正确的,关键就是状态转移方程怎么找呢?还得从我们原始的思路开始讲。之前的解法配了这样图帮助大家理解状态转移思路: + +![](https://labuladong.online/algo/images/drop-egg/1.jpg) + +这个图描述的仅仅是某一个楼层 `i`,原始解法还得线性或者二分扫描所有楼层,要求最大值、最小值。但是现在这种 `dp` 定义根本不需要这些了,基于下面两个事实: + +**1、无论你在哪层楼扔鸡蛋,鸡蛋只可能摔碎或者没摔碎,碎了的话就测楼下,没碎的话就测楼上**。 + +**2、无论你上楼还是下楼,总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)**。 + +根据这个特点,可以写出下面的状态转移方程: ```python -def superEggDrop(self, K: int, N: int) -> int: - - memo = dict() - def dp(K, N): - if K == 1: return N - if N == 0: return 0 - if (K, N) in memo: - return memo[(K, N)] - - # for 1 <= i <= N: - # res = min(res, - # max( - # dp(K - 1, i - 1), - # dp(K, N - i) - # ) + 1 - # ) - - res = float('INF') - # 用二分搜索代替线性搜索 - lo, hi = 1, N - while lo <= hi: - mid = (lo + hi) // 2 - broken = dp(K - 1, mid - 1) # 碎 - not_broken = dp(K, N - mid) # 没碎 - # res = min(max(碎,没碎) + 1) - if broken > not_broken: - hi = mid - 1 - res = min(res, broken + 1) - else: - lo = mid + 1 - res = min(res, not_broken + 1) - - memo[(K, N)] = res - return res +dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1 +``` + +**`dp[k][m - 1]` 就是楼上的楼层数**,因为鸡蛋个数 `k` 不变,也就是鸡蛋没碎,扔鸡蛋次数 `m` 减一; + +**`dp[k - 1][m - 1]` 就是楼下的楼层数**,因为鸡蛋个数 `k` 减一,也就是鸡蛋碎了,同时扔鸡蛋次数 `m` 减一。 + +> [!NOTE] +> 这个 `m` 为什么要减一而不是加一?之前定义得很清楚,这个 `m` 是一个允许扔鸡蛋的次数上界,而不是扔了几次。 + +![](https://labuladong.online/algo/images/drop-egg/3.jpg) + +至此,整个思路就完成了,只要把状态转移方程填进框架即可: + +```java +class Solution { + public int superEggDrop(int K, int N) { + // m 最多不会超过 N 次(线性扫描) + int[][] dp = new int[K + 1][N + 1]; + // base case: + // dp[0][..] = 0 + // dp[..][0] = 0 + // Java 默认初始化数组都为 0 + int m = 0; + while (dp[K][m] < N) { + m++; + for (int k = 1; k <= K; k++) + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; + } + return m; + } +} +``` + + +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +如果你还觉得这段代码有点难以理解,其实它就等同于这样写: + +```java +for (int m = 1; dp[K][m] < N; m++) + for (int k = 1; k <= K; k++) + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; +``` + +看到这种代码形式就熟悉多了吧,因为我们要求的不是 `dp` 数组里的值,而是某个符合条件的索引 `m`,所以用 `while` 循环来找到这个 `m` 而已。 + +这个算法的时间复杂度是多少?很明显就是两个嵌套循环的复杂度 O(KN)。 + +另外注意到 `dp[m][k]` 转移只和左边和左上的两个状态有关,可以根据前文 [动态规划的空间压缩技巧](https://labuladong.online/algo/dynamic-programming/space-optimization/) 优化成一维 `dp` 数组,这里就不写了。 + +## 五、还可以再优化 + +再往下还可以继续优化,我就不具体展开了,仅仅简单提一下思路吧。 + +在刚才的思路之上,**注意函数 `dp(m, k)` 是随着 `m` 单增的,因为鸡蛋个数 `k` 不变时,允许的测试次数越多,可测试的楼层就越高**。 + +这里又可以借助二分搜索算法快速逼近 `dp[K][m] == N` 这个终止条件,时间复杂度进一步下降为 O(KlogN)。不过我觉得我们能够写出 O(K\*N\*logN) 的二分优化算法就行了,后面的这些解法呢,我认为不太有必要掌握,把欲望限制在能力的范围之内才能拥有快乐! + +不过可以肯定的是,根据二分搜索代替线性扫描 `m` 的取值,代码的大致框架肯定是修改穷举 `m` 的 while 循环: + +```java +// 把线性搜索改成二分搜索 +// for (int m = 1; dp[K][m] < N; m++) +int lo = 1, hi = N; +while (lo < hi) { + int mid = (lo + hi) / 2; + if (... < N) { + lo = ... + } else { + hi = ... + } - return dp(K, N) + for (int k = 1; k <= K; k++) { + // 状态转移方程 + } +} ``` -这里就不展开其他解法了,留在下一篇文章 [高楼扔鸡蛋进阶](高楼扔鸡蛋进阶.md) +简单总结一下吧,第一个二分优化是利用了 `dp` 函数的单调性,用二分查找技巧快速搜索答案;第二种优化是巧妙地修改了状态转移方程,简化了求解了流程,但相应的,解题逻辑比较难以想到;后续还可以用一些数学方法和二分搜索进一步优化第二种解法,不过不太值得掌握。 + + + + + + + +
+
+引用本文的文章 + + - [实际运用二分搜索时的思维框架](https://labuladong.online/algo/frequency-interview/binary-search-in-action/) + - [最优子结构原理和 dp 数组遍历方向](https://labuladong.online/algo/dynamic-programming/faq-summary/) + - [经典动态规划:戳气球](https://labuladong.online/algo/dynamic-programming/burst-balloons/) + +

+ -我觉得吧,我们这种解法就够了:找状态,做选择,足够清晰易懂,可流程化,可举一反三。掌握这套框架学有余力的话,再去考虑那些奇技淫巧也不迟。 -最后预告一下,《动态规划详解(修订版)》和《回溯算法详解(修订版)》已经动笔了,教大家用模板的力量来对抗变化无穷的算法题,敬请期待。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:编辑距离](../动态规划系列/编辑距离.md) -[下一篇:经典动态规划问题:高楼扔鸡蛋(进阶)](../动态规划系列/高楼扔鸡蛋进阶.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\255\224\345\241\224.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\255\224\345\241\224.md" new file mode 100644 index 0000000000..c8353f7f5a --- /dev/null +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\255\224\345\241\224.md" @@ -0,0 +1,262 @@ +# 动态规划帮我通关了《魔塔》 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [174. Dungeon Game](https://leetcode.com/problems/dungeon-game/) | [174. 地下城游戏](https://leetcode.cn/problems/dungeon-game/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) + +「魔塔」是一款经典的地牢类游戏,碰怪物要掉血,吃血瓶能加血,你要收集钥匙,一层一层上楼,最后救出美丽的公主。 + +现在手机上仍然可以玩这个游戏: + +![](https://labuladong.online/algo/images/dungeons/0.png) + +嗯,相信这款游戏承包了不少人的童年回忆,记得小时候,一个人拿着游戏机玩,两三个人围在左右指手画脚,这导致玩游戏的人体验极差,而左右的人异常快乐 😂 + +力扣第 174 题「地下城游戏」是一道类似的题目: + + + +**简单说,就是问你至少需要多少初始生命值,能够让骑士从最左上角移动到最右下角,且任何时候生命值都要大于 0**。 + +函数签名如下: + +```java +int calculateMinimumHP(int[][] grid); +``` + +上篇文章 [最小路径和](https://labuladong.online/algo/dynamic-programming/minimum-path-sum/) 写过类似的问题,问你从左上角到右下角的最小路径和是多少。 + +我们做算法题一定要尝试举一反三,感觉今天这道题和最小路径和有点关系对吧? + +想要最小化骑士的初始生命值,是不是意味着要最大化骑士行进路线上的血瓶?是不是相当于求「最大路径和」?是不是可以直接套用计算「最小路径和」的思路? + +但是稍加思考,发现这个推论并不成立,吃到最多的血瓶,并不一定就能获得最小的初始生命值。 + +比如如下这种情况,如果想要吃到最多的血瓶获得「最大路径和」,应该按照下图箭头所示的路径,初始生命值需要 11: + +![](https://labuladong.online/algo/images/dungeons/2.png) + +但也很容易看到,正确的答案应该是下图箭头所示的路径,初始生命值只需要 1: + +![](https://labuladong.online/algo/images/dungeons/3.png) + +**所以,关键不在于吃最多的血瓶,而是在于如何损失最少的生命值**。 + +这类求最值的问题,肯定要借助动态规划技巧,要合理设计 `dp` 数组/函数的定义。类比前文 [最小路径和问题](https://labuladong.online/algo/dynamic-programming/minimum-path-sum/),`dp` 函数签名肯定长这样: + +```java +int dp(int[][] grid, int i, int j); +``` + +但是这道题对 `dp` 函数的定义比较有意思,按照常理,这个 `dp` 函数的定义应该是: + +**从左上角(`grid[0][0]`)走到 `grid[i][j]` 至少需要 `dp(grid, i, j)` 的生命值**。 + +这样定义的话,base case 就是 `i, j` 都等于 0 的时候,我们可以这样写代码: + +```java +int calculateMinimumHP(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + // 我们想计算左上角到右下角所需的最小生命值 + return dp(grid, m - 1, n - 1); +} + +int dp(int[][] grid, int i, int j) { + // base case + if (i == 0 && j == 0) { + // 保证骑士落地不死就行了 + return grid[i][j] > 0 ? 1 : -grid[i][j] + 1; + } + ... +} +``` + +> [!NOTE] +> 为了简洁,之后 `dp(grid, i, j)` 就简写为 `dp(i, j)`,大家理解就好。 + +接下来我们需要找状态转移了,还记得如何找状态转移方程吗?我们这样定义 `dp` 函数能否正确进行状态转移呢? + +我们希望 `dp(i, j)` 能够通过 `dp(i-1, j)` 和 `dp(i, j-1)` 推导出来,这样就能不断逼近 base case,也就能够正确进行状态转移。 + +具体来说,「到达 `A` 的最小生命值」应该能够由「到达 `B` 的最小生命值」和「到达 `C` 的最小生命值」推导出来: + +![](https://labuladong.online/algo/images/dungeons/4.png) + +**但问题是,能推出来么?实际上是不能的**。 + +因为按照 `dp` 函数的定义,你只知道「能够从左上角到达 `B` 的最小生命值」,但并不知道「到达 `B` 时的生命值」。 + +「到达 `B` 时的生命值」是进行状态转移的必要参考,我给你举个例子你就明白了,假设下图这种情况: + +![](https://labuladong.online/algo/images/dungeons/5.png) + +你说这种情况下,骑士救公主的最优路线是什么? + +显然是按照图中蓝色的线走到 `B`,最后走到 `A` 对吧,这样初始血量只需要 1 就可以;如果走黄色箭头这条路,先走到 `C` 然后走到 `A`,初始血量至少需要 6。 + +为什么会这样呢?骑士走到 `B` 和 `C` 的最少初始血量都是 1,为什么最后是从 `B` 走到 `A`,而不是从 `C` 走到 `A` 呢? + +因为骑士走到 `B` 的时候生命值为 11,而走到 `C` 的时候生命值依然是 1。 + +如果骑士执意要通过 `C` 走到 `A`,那么初始血量必须加到 6 点才行;而如果通过 `B` 走到 `A`,初始血量为 1 就够了,因为路上吃到血瓶了,生命值足够抗 `A` 上面怪物的伤害。 + +这下应该说的很清楚了,再回顾我们对 `dp` 函数的定义,上图的情况,算法只知道 `dp(1, 2) = dp(2, 1) = 1`,都是一样的,怎么做出正确的决策,计算出 `dp(2, 2)` 呢? + +**所以说,我们之前对 `dp` 数组的定义是错误的,信息量不足,算法无法做出正确的状态转移**。 + +正确的做法需要反向思考,依然是如下的 `dp` 函数: + +```java +int dp(int[][] grid, int i, int j); +``` + +但是我们要修改 `dp` 函数的定义: + +**从 `grid[i][j]` 到达终点(右下角)所需的最少生命值是 `dp(grid, i, j)`**。 + +那么可以这样写代码: + +```java +int calculateMinimumHP(int[][] grid) { + // 我们想计算左上角到右下角所需的最小生命值 + return dp(grid, 0, 0); +} + +int dp(int[][] grid, int i, int j) { + int m = grid.length; + int n = grid[0].length; + // base case + if (i == m - 1 && j == n - 1) { + return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; + } + ... +} +``` + +根据新的 `dp` 函数定义和 base case,我们想求 `dp(0, 0)`,那就应该试图通过 `dp(i, j+1)` 和 `dp(i+1, j)` 推导出 `dp(i, j)`,这样才能不断逼近 base case,正确进行状态转移。 + +具体来说,「从 `A` 到达右下角的最少生命值」应该由「从 `B` 到达右下角的最少生命值」和「从 `C` 到达右下角的最少生命值」推导出来: + +![](https://labuladong.online/algo/images/dungeons/6.png) + +能不能推导出来呢?这次是可以的,假设 `dp(0, 1) = 5, dp(1, 0) = 4`,那么可以肯定要从 `A` 走向 `C`,因为 4 小于 5 嘛。 + +那么怎么推出 `dp(0, 0)` 是多少呢? + +假设 `A` 的值为 1,既然知道下一步要往 `C` 走,且 `dp(1, 0) = 4` 意味着走到 `grid[1][0]` 的时候至少要有 4 点生命值,那么就可以确定骑士出现在 `A` 点时需要 4 - 1 = 3 点初始生命值,对吧。 + +那如果 `A` 的值为 10,落地就能捡到一个大血瓶,超出了后续需求,4 - 10 = -6 意味着骑士的初始生命值为负数,这显然不可以,骑士的生命值小于 1 就挂了,所以这种情况下骑士的初始生命值应该是 1。 + +综上,状态转移方程已经推出来了: + + + + + +```java +int res = min( + dp(i + 1, j), + dp(i, j + 1) +) - grid[i][j]; + +dp(i, j) = res <= 0 ? 1 : res; +``` + +根据这个核心逻辑,加一个备忘录消除重叠子问题,就可以直接写出最终的代码了: + +```java +class Solution { + // 主函数 + public int calculateMinimumHP(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + // 备忘录中都初始化为 -1 + memo = new int[m][n]; + for (int[] row : memo) { + Arrays.fill(row, -1); + } + + return dp(grid, 0, 0); + } + + // 备忘录,消除重叠子问题 + int[][] memo; + + // 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 + int dp(int[][] grid, int i, int j) { + int m = grid.length; + int n = grid[0].length; + // base case + if (i == m - 1 && j == n - 1) { + return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; + } + if (i == m || j == n) { + return Integer.MAX_VALUE; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 状态转移逻辑 + int res = Math.min( + dp(grid, i, j + 1), + dp(grid, i + 1, j) + ) - grid[i][j]; + // 骑士的生命值至少为 1 + memo[i][j] = res <= 0 ? 1 : res; + + return memo[i][j]; + } +} +``` + + +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +这就是自顶向下带备忘录的动态规划解法,参考前文 [动态规划套路详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 很容易就可以改写成 `dp` 数组的迭代解法,这里就不写了,读者可以尝试自己写一写。 + +这道题的核心是定义 `dp` 函数,找到正确的状态转移方程,从而计算出正确的答案。 + + + + + + + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\345\244\232\350\257\255\350\250\200\350\247\243\346\263\225\344\273\243\347\240\201/contribution-guide.md" "b/\345\244\232\350\257\255\350\250\200\350\247\243\346\263\225\344\273\243\347\240\201/contribution-guide.md" new file mode 100644 index 0000000000..0336bb3d4c --- /dev/null +++ "b/\345\244\232\350\257\255\350\250\200\350\247\243\346\263\225\344\273\243\347\240\201/contribution-guide.md" @@ -0,0 +1,74 @@ +# 修正 labuladong 刷题插件中的错误 + +## 背景 + +为了帮助大家更好地学习算法,我之前写了很多算法教程,并开发了一系列刷题插件,统称为《labuladong 的刷题全家桶》,详情见 [这里](https://labuladong.github.io/article/fname.html?fname=全家桶简介)。 + +在我的教程和插件中的解法主要使用的是 Java 语言,原因是 Java 这门语言中规中矩,就算之前没有接触过,也能比较容易看懂逻辑。不过现在这不是 chatGPT 横空出世了嘛,我就借助 chatGPT 把我的解法改写成多种语言,希望对不同技术背景的小伙伴更加友好。 + +chatGPT 的改写效果还是非常不错的,不过难免还是存在一些错误,所以我希望能够和大家一起来修正这些错误。 + +## 如何反馈错误 + +如果你发现某些解法代码不能通过力扣的所有测试用例(一般都是 chatGPT 改写的解法代码会出现这种情况,我的解法代码都是通过测试才发布的),可以 [点这里](https://github.com/labuladong/fucking-algorithm/issues/new?assignees=&labels=code+bug&template=bug_report.yml&title=%5Bbug%5D%5B%7B%E8%BF%99%E9%87%8C%E6%9B%BF%E6%8D%A2%E4%B8%BA%E5%87%BA%E9%94%99%E7%9A%84%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%7D%5D+%7B%E8%BF%99%E9%87%8C%E6%9B%BF%E6%8D%A2%E4%B8%BA%E5%87%BA%E9%94%99%E7%9A%84%E5%8A%9B%E6%89%A3%E9%A2%98%E7%9B%AE%E6%A0%87%E8%AF%86%E7%AC%A6%7D+) 按照模板提交 issue,我和其他小伙伴会提交 PR 修复这些错误。 + +## 如何修正错误 + +首先,感谢你愿意为我的插件提供的解法代码纠错,你向本仓库提交 PR 修复错误后,你将成为本仓库的 contributor,出现在仓库首页的贡献者列表中。本仓库已经获得了 115k star,你的贡献将会被许多人看到。 + +修复代码很简单,所有多语言解法代码都存储在 [多语言解法代码/solution_code.md](https://github.com/labuladong/fucking-algorithm/blob/master/%E5%A4%9A%E8%AF%AD%E8%A8%80%E8%A7%A3%E6%B3%95%E4%BB%A3%E7%A0%81/solution_code.md) 中,你只要修改这个文件就行了。其内容的组织形如如下: + + + https://leetcode.cn/problems/xxx 的多语言解法👇 + + ```cpp + class Solution { + public: + int xxx() { + // ... + } + }; + ``` + + ```java + class Solution { + public int xxx() { + // ... + } + } + ``` + + ```python + class Solution: + def xxx(self): + # ... + ``` + + ```javascript + var xxx = function() { + // ... + } + ``` + + ```go + func xxx() { + // ... + } + ``` + + https://leetcode.cn/problems/xxx 的多语言解法👆 + + +比如你想修改 [https://leetcode-cn.com/problems/longest-palindromic-substring/](https://leetcode-cn.com/problems/longest-palindromic-substring/) 的 JavaScript 解法,你可以在 [多语言解法代码/solution_code.md](https://github.com/labuladong/fucking-algorithm/blob/master/%E5%A4%9A%E8%AF%AD%E8%A8%80%E8%A7%A3%E6%B3%95%E4%BB%A3%E7%A0%81/solution_code.md) 中搜索 `longest-palindromic-substring` 关键词,即可找到这道题的多语言解法,然后修改 JavaScript 对应的解法代码,提交 PR 即可。 + +我的插件会自动拉取这个文件的最新内容,所以你的 PR 被合进 master 分支后,插件中的内容修改也会生效。 + +## 提交 PR 的要求 + +1、你的 PR 必须是针对 [多语言解法代码/solution_code.md](https://github.com/labuladong/fucking-algorithm/blob/master/%E5%A4%9A%E8%AF%AD%E8%A8%80%E8%A7%A3%E6%B3%95%E4%BB%A3%E7%A0%81/solution_code.md) 文件中代码部分的修改,不要修改其他文件和其他内容。 + +2、把我的解法翻译成多语言的目的是帮助不同背景的小伙伴理解算法思维,所以你修改的代码可以不是效率最优的,但应该尽可能和我的解法思路保持一致,且包含我的解法中的完整注释。 + +3、你的 PR 描述中需要包含代码通过所有测试用例截图。PR 标题的格式为 `[fix][{lang}] {slug}`,其中 `{lang}` 需要替换为你修复的解法语言,比如 `[fix][cpp]`,`{slug}` 需要替换为你修复的题目的标识符(题目 URL 的最后一部分),比如 [https://leetcode.cn/problems/search-a-2d-matrix/](https://leetcode.cn/problems/search-a-2d-matrix/) 这道题的标识符就是 `search-a-2d-matrix`。 + +**你可以查看这个 PR 作为案例**:https://github.com/labuladong/fucking-algorithm/pull/1112 \ No newline at end of file diff --git "a/\345\244\232\350\257\255\350\250\200\350\247\243\346\263\225\344\273\243\347\240\201/solution_code.md" "b/\345\244\232\350\257\255\350\250\200\350\247\243\346\263\225\344\273\243\347\240\201/solution_code.md" new file mode 100644 index 0000000000..900137b7e7 --- /dev/null +++ "b/\345\244\232\350\257\255\350\250\200\350\247\243\346\263\225\344\273\243\347\240\201/solution_code.md" @@ -0,0 +1,72142 @@ +https://leetcode.cn/problems/01-matrix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> updateMatrix(vector>& mat) { + int m = mat.size(), n = mat[0].size(); + // 记录答案的结果数组 + vector> res(m, vector(n, -1)); + // 初始化队列,把那些值为 0 的坐标放到队列里 + queue> q; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (mat[i][j] == 0) { + q.push({i, j}); + res[i][j] = 0; + } + } + } + // 执行 BFS 算法框架,从值为 0 的坐标开始向四周扩散 + vector> dirs{{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + while (!q.empty()) { + auto cur = q.front(); + q.pop(); + int x = cur.first, y = cur.second; + // 向四周扩散 + for (auto& dir : dirs) { + int nextX = x + dir[0]; + int nextY = y + dir[1]; + // 确保相邻的这个坐标没有越界且之前未被计算过 + if (nextX >= 0 && nextX < m && nextY >= 0 && nextY < n + && res[nextX][nextY] == -1) { + q.push({nextX, nextY}); + // 从 mat[x][y] 走到 mat[nextX][nextY] 需要一步 + res[nextX][nextY] = res[x][y] + 1; + } + } + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func updateMatrix(mat [][]int) [][]int { + m, n := len(mat), len(mat[0]) + // 记录答案的结果数组 + res := make([][]int, m) + for i := range res { + res[i] = make([]int, n) + for j := range res[i] { + res[i][j] = -1 + } + } + // 初始化队列,把那些值为 0 的坐标放到队列里 + q := make([][2]int, 0) + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if mat[i][j] == 0 { + q = append(q, [2]int{i, j}) + res[i][j] = 0 + } + } + } + // 执行 BFS 算法框架,从值为 0 的坐标开始向四周扩散 + dirs := [][]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}} + for len(q) > 0 { + cur := q[0] + q = q[1:] + x, y := cur[0], cur[1] + // 向四周扩散 + for _, dir := range dirs { + nextX, nextY := x+dir[0], y+dir[1] + // 确保相邻的这个坐标没有越界且之前未被计算过 + if nextX >= 0 && nextX < m && nextY >= 0 && nextY < n && res[nextX][nextY] == -1 { + q = append(q, [2]int{nextX, nextY}) + // 从 mat[x][y] 走到 mat[nextX][nextY] 需要一步 + res[nextX][nextY] = res[x][y] + 1 + } + } + } + + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] updateMatrix(int[][] mat) { + int m = mat.length, n = mat[0].length; + // 记录答案的结果数组 + int[][] res = new int[m][n]; + // 初始化全部填充特殊值 -1,代表未计算, + // 待会可以用来判断坐标是否已经计算过,避免重复遍历 + for (int[] row : res) { + Arrays.fill(row, -1); + } + + Queue q = new LinkedList<>(); + // 初始化队列,把那些值为 0 的坐标放到队列里 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (mat[i][j] == 0) { + q.offer(new int[]{i, j}); + res[i][j] = 0; + } + } + } + // 执行 BFS 算法框架,从值为 0 的坐标开始向四周扩散 + int[][] dirs = new int[][]{{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + while (!q.isEmpty()) { + int[] cur = q.poll(); + int x = cur[0], y = cur[1]; + // 向四周扩散 + for (int i = 0; i < 4; i++) { + int nextX = x + dirs[i][0]; + int nextY = y + dirs[i][1]; + // 确保相邻的这个坐标没有越界且之前未被计算过 + if (nextX >= 0 && nextX < m && nextY >= 0 && nextY < n + && res[nextX][nextY] == -1) { + q.offer(new int[]{nextX, nextY}); + // 从 mat[x][y] 走到 mat[nextX][nextY] 需要一步 + res[nextX][nextY] = res[x][y] + 1; + } + } + } + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +function updateMatrix(mat) { + const m = mat.length, n = mat[0].length; + // 记录答案的结果数组 + const res = new Array(m).fill().map(() => new Array(n).fill(-1)); + + const q = []; + // 初始化队列,把那些值为 0 的坐标放到队列里 + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (mat[i][j] === 0) { + q.push([i, j]); + res[i][j] = 0; + } + } + } + // 执行 BFS 算法框架,从值为 0 的坐标开始向四周扩散 + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + while (q.length) { + const cur = q.shift(); + const x = cur[0], y = cur[1]; + // 向四周扩散 + for (let i = 0; i < 4; i++) { + const nextX = x + dirs[i][0]; + const nextY = y + dirs[i][1]; + // 确保相邻的这个坐标没有越界且之前未被计算过 + if (nextX >= 0 && nextX < m && nextY >= 0 && nextY < n + && res[nextX][nextY] === -1) { + q.push([nextX, nextY]); + // 从 mat[x][y] 走到 mat[nextX][nextY] 需要一步 + res[nextX][nextY] = res[x][y] + 1; + } + } + } + + return res; +} +``` + +```python +# by chatGPT (python) +from collections import deque + +class Solution: + def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]: + m, n = len(mat), len(mat[0]) + # 记录答案的结果数组 + res = [[-1] * n for _ in range(m)] + + q = deque() + # 初始化队列,把那些值为 0 的坐标放到队列里 + for i in range(m): + for j in range(n): + if mat[i][j] == 0: + q.append((i, j)) + res[i][j] = 0 + + # 执行 BFS 算法框架,从值为 0 的坐标开始向四周扩散 + dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]] + while q: + x, y = q.popleft() + # 向四周扩散 + for dx, dy in dirs: + nextX, nextY = x + dx, y + dy + # 确保相邻的这个坐标没有越界且之前未被计算过 + if 0 <= nextX < m and 0 <= nextY < n and res[nextX][nextY] == -1: + q.append((nextX, nextY)) + # 从 mat[x][y] 走到 mat[nextX][nextY] 需要一步 + res[nextX][nextY] = res[x][y] + 1 + + return res +``` + +https://leetcode.cn/problems/01-matrix 的多语言解法👆 + +https://leetcode.cn/problems/0i0mDW 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int memo[201][201]; + + int minPathSum(vector>& grid) { + int m = grid.size(); + int n = grid[0].size(); + // 构造备忘录,初始值全部设为 -1 + memset(memo, -1, sizeof(memo)); + + return dp(grid, m - 1, n - 1); + } + + int dp(vector>& grid, int i, int j) { + // base case + if (i == 0 && j == 0) { + return grid[0][0]; + } + if (i < 0 || j < 0) { + return INT_MAX; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 将计算结果记入备忘录 + memo[i][j] = min( + dp(grid, i - 1, j), + dp(grid, i, j - 1) + ) + grid[i][j]; + + return memo[i][j]; + } +}; +``` + +```go +// by chatGPT (go) +func minPathSum(grid [][]int) int { + m := len(grid) + n := len(grid[0]) + // 构造备忘录,初始值全部设为 -1 + memo := make([][]int, m) + for i := 0; i < m; i++ { + memo[i] = make([]int, n) + for j := 0; j < n; j++ { + memo[i][j] = -1 + } + } + + return dp(grid, m - 1, n - 1, memo) +} + +func dp(grid [][]int, i int, j int, memo [][]int) int { + // base case + if i == 0 && j == 0 { + return grid[0][0] + } + if i < 0 || j < 0 { + return math.MaxInt32 + } + // 避免重复计算 + if memo[i][j] != -1 { + return memo[i][j] + } + // 将计算结果记入备忘录 + left := dp(grid, i - 1, j, memo) + up := dp(grid, i, j - 1, memo) + curr := grid[i][j] + min(left, up) + memo[i][j] = curr + return curr +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + int[][] memo; + + public int minPathSum(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + // 构造备忘录,初始值全部设为 -1 + memo = new int[m][n]; + for (int[] row : memo) + Arrays.fill(row, -1); + + return dp(grid, m - 1, n - 1); + } + + int dp(int[][] grid, int i, int j) { + // base case + if (i == 0 && j == 0) { + return grid[0][0]; + } + if (i < 0 || j < 0) { + return Integer.MAX_VALUE; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 将计算结果记入备忘录 + memo[i][j] = Math.min( + dp(grid, i - 1, j), + dp(grid, i, j - 1) + ) + grid[i][j]; + + return memo[i][j]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var minPathSum = function(grid) { + var m = grid.length; + var n = grid[0].length; + // 构造备忘录,初始值全部设为 -1 + var memo = new Array(m); + for (var i = 0; i < memo.length; i++) { + memo[i] = new Array(n); + memo[i].fill(-1); + } + + return dp(grid, m - 1, n - 1, memo); +}; + +function dp(grid, i, j, memo) { + // base case + if (i == 0 && j == 0) { + return grid[0][0]; + } + if (i < 0 || j < 0) { + return Number.MAX_VALUE; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 将计算结果记入备忘录 + memo[i][j] = Math.min( + dp(grid, i - 1, j, memo), + dp(grid, i, j - 1, memo) + ) + grid[i][j]; + + return memo[i][j]; +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.memo = None + + def minPathSum(self, grid: List[List[int]]) -> int: + m = len(grid) + n = len(grid[0]) + # 构造备忘录,初始值全部设为 -1 + self.memo = [[-1 for _ in range(n)] for _ in range(m)] + + return self.dp(grid, m - 1, n - 1) + + def dp(self, grid: List[List[int]], i: int, j: int) -> int: + # base case + if i == 0 and j == 0: + return grid[0][0] + if i < 0 or j < 0: + return float('inf') + # 避免重复计算 + if self.memo[i][j] != -1: + return self.memo[i][j] + # 将计算结果记入备忘录 + self.memo[i][j] = min( + self.dp(grid, i - 1, j), + self.dp(grid, i, j - 1) + ) + grid[i][j] + + return self.memo[i][j] +``` + +https://leetcode.cn/problems/0i0mDW 的多语言解法👆 + +https://leetcode.cn/problems/1fGaJU 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector> threeSum(vector& nums) { + sort(nums.begin(), nums.end()); + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0); + } + + /* 注意:调用这个函数之前一定要先给 nums 排序 */ + // n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + vector> nSumTarget( + vector& nums, int n, int start, int target) { + + int sz = nums.size(); + vector> res; + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n == 2) { + // 双指针那一套操作 + int lo = start, hi = sz - 1; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + int left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] == left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] == right) hi--; + } else { + res.push_back({left, right}); + while (lo < hi && nums[lo] == left) lo++; + while (lo < hi && nums[hi] == right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (int i = start; i < sz; i++) { + vector> + sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (vector& arr : sub) { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr.push_back(nums[i]); + res.push_back(arr); + } + while (i < sz - 1 && nums[i] == nums[i + 1]) i++; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func threeSum(nums []int) [][]int { + sort.Ints(nums) + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0) +} + +/* 注意:调用这个函数之前一定要先给 nums 排序 */ +// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 +func nSumTarget(nums []int, n int, start int, target int) [][]int { + sz := len(nums) + var res [][]int + // 至少是 2Sum,且数组大小不应该小于 n + if n < 2 || sz < n { + return res + } + // 2Sum 是 base case + if n == 2 { + // 双指针那一套操作 + lo, hi := start, sz-1 + for lo < hi { + sum := nums[lo] + nums[hi] + left, right := nums[lo], nums[hi] + if sum < target { + for lo < hi && nums[lo] == left { + lo++ + } + } else if sum > target { + for lo < hi && nums[hi] == right { + hi-- + } + } else { + res = append(res, []int{left, right}) + for lo < hi && nums[lo] == left { + lo++ + } + for lo < hi && nums[hi] == right { + hi-- + } + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for i := start; i < sz; i++ { + sub := nSumTarget(nums, n-1, i+1, target-nums[i]) + for _, arr := range sub { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr = append(arr, nums[i]) + res = append(res, arr) + } + for i < sz-1 && nums[i] == nums[i+1] { + i++ + } + } + } + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + public List> threeSum(int[] nums) { + Arrays.sort(nums); + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0); + } + + /* 注意:调用这个函数之前一定要先给 nums 排序 */ + // n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + public List> nSumTarget( + int[] nums, int n, int start, int target) { + + int sz = nums.length; + List> res = new ArrayList<>(); + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n == 2) { + // 双指针那一套操作 + int lo = start, hi = sz - 1; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + int left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] == left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] == right) hi--; + } else { + List triplet = new ArrayList<>(); + triplet.add(left); + triplet.add(right); + res.add(triplet); + while (lo < hi && nums[lo] == left) lo++; + while (lo < hi && nums[hi] == right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (int i = start; i < sz; i++) { + List> sub = + nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (List arr : sub) { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr.add(nums[i]); + res.add(arr); + } + while (i < sz - 1 && nums[i] == nums[i + 1]) i++; + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var threeSum = function(nums) { + nums.sort((a, b) => a - b); + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0); +}; + +/* 注意:调用这个函数之前一定要先给 nums 排序 */ +// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 +var nSumTarget = function(nums, n, start, target) { + var sz = nums.length; + var res = []; + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n === 2) { + // 双指针那一套操作 + var lo = start, hi = sz - 1; + while (lo < hi) { + var sum = nums[lo] + nums[hi]; + var left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] === left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] === right) hi--; + } else { + res.push([left, right]); + while (lo < hi && nums[lo] === left) lo++; + while (lo < hi && nums[hi] === right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (var i = start; i < sz; i++) { + var sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (var j = 0; j < sub.length; j++) { + // (n-1)Sum 加上 nums[i] 就是 nSum + sub[j].push(nums[i]); + res.push(sub[j]); + } + while (i < sz - 1 && nums[i] === nums[i + 1]) i++; + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def threeSum(self, nums: List[int]) -> List[List[int]]: + nums.sort() + # n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return self.nSumTarget(nums, 3, 0, 0) + + # 注意:调用这个函数之前一定要先给 nums 排序 + # n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + def nSumTarget(self, nums: List[int], n: int, start: int, target: int) -> List[List[int]]: + sz = len(nums) + res = [] + # 至少是 2Sum,且数组大小不应该小于 n + if n < 2 or sz < n: + return res + # 2Sum 是 base case + if n == 2: + # 双指针那一套操作 + lo, hi = start, sz - 1 + while lo < hi: + _sum = nums[lo] + nums[hi] + left, right = nums[lo], nums[hi] + if _sum < target: + while lo < hi and nums[lo] == left: + lo += 1 + elif _sum > target: + while lo < hi and nums[hi] == right: + hi -= 1 + else: + res.append([left, right]) + while lo < hi and nums[lo] == left: + lo += 1 + while lo < hi and nums[hi] == right: + hi -= 1 + else: + # n > 2 时,递归计算 (n-1)Sum 的结果 + for i in range(start, sz): + if i > start and nums[i] == nums[i - 1]: + continue + sub = self.nSumTarget(nums, n - 1, i + 1, target - nums[i]) + for arr in sub: + # (n-1)Sum 加上 nums[i] 就是 nSum + arr.append(nums[i]) + res.append(arr) + return res +``` + +https://leetcode.cn/problems/1fGaJU 的多语言解法👆 + +https://leetcode.cn/problems/2AoeFn 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 备忘录 + int memo[100][100]; + + int uniquePaths(int m, int n) { + memset(memo, 0, sizeof(memo)); + return dp(m - 1, n - 1); + } + + // 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 + int dp(int x, int y) { + // base case + if (x == 0 && y == 0) { + return 1; + } + if (x < 0 || y < 0) { + return 0; + } + // 避免冗余计算 + if (memo[x][y] > 0) { + return memo[x][y]; + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x - 1, y) + dp(x, y - 1); + return memo[x][y]; + } +}; +``` + +```go +// by chatGPT (go) +func uniquePaths(m int, n int) int { + // 备忘录 + memo := make([][]int, m) + for i := range memo { + memo[i] = make([]int, n) + } + return dp(m-1, n-1, memo) +} + +// 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 +func dp(x int, y int, memo [][]int) int { + // base case + if x == 0 && y == 0 { + return 1 + } + if x < 0 || y < 0 { + return 0 + } + // 避免冗余计算 + if memo[x][y] > 0 { + return memo[x][y] + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x-1, y, memo) + dp(x, y-1, memo) + return memo[x][y] +} +``` + +```java +// by labuladong (java) +class Solution { + // 备忘录 + int[][] memo; + + public int uniquePaths(int m, int n) { + memo = new int[m][n]; + return dp(m - 1, n - 1); + } + + // 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 + int dp(int x, int y) { + // base case + if (x == 0 && y == 0) { + return 1; + } + if (x < 0 || y < 0) { + return 0; + } + // 避免冗余计算 + if (memo[x][y] > 0) { + return memo[x][y]; + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x - 1, y) + dp(x, y - 1); + return memo[x][y]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var uniquePaths = function(m, n) { + // 备忘录 + let memo = new Array(m).fill().map(() => new Array(n).fill(0)); + + // 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 + var dp = function(x, y) { + // base case + if (x === 0 && y === 0) { + return 1; + } + if (x < 0 || y < 0) { + return 0; + } + // 避免冗余计算 + if (memo[x][y] > 0) { + return memo[x][y]; + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x - 1, y) + dp(x, y - 1); + return memo[x][y]; + }; + + return dp(m - 1, n - 1); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.memo = None + + def uniquePaths(self, m: int, n: int) -> int: + self.memo = [[0] * n for _ in range(m)] + return self.dp(m-1, n-1) + + def dp(self, x: int, y: int) -> int: + # base case + if x == 0 and y == 0: + return 1 + if x < 0 or y < 0: + return 0 + # 避免冗余计算 + if self.memo[x][y] > 0: + return self.memo[x][y] + # 状态转移方程: + # 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + self.memo[x][y] = self.dp(x - 1, y) + self.dp(x, y - 1) + return self.memo[x][y] +``` + +https://leetcode.cn/problems/2AoeFn 的多语言解法👆 + +https://leetcode.cn/problems/3sum 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector> threeSum(vector& nums) { + sort(nums.begin(), nums.end()); + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0); + } + + /* 注意:调用这个函数之前一定要先给 nums 排序 */ + // n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + vector> nSumTarget( + vector& nums, int n, int start, int target) { + + int sz = nums.size(); + vector> res; + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n == 2) { + // 双指针那一套操作 + int lo = start, hi = sz - 1; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + int left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] == left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] == right) hi--; + } else { + res.push_back({left, right}); + while (lo < hi && nums[lo] == left) lo++; + while (lo < hi && nums[hi] == right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (int i = start; i < sz; i++) { + vector> + sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (vector& arr : sub) { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr.push_back(nums[i]); + res.push_back(arr); + } + while (i < sz - 1 && nums[i] == nums[i + 1]) i++; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func threeSum(nums []int) [][]int { + sort.Ints(nums) + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0) +} + +/* 注意:调用这个函数之前一定要先给 nums 排序 */ +// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 +func nSumTarget(nums []int, n int, start int, target int) [][]int { + sz := len(nums) + var res [][]int + // 至少是 2Sum,且数组大小不应该小于 n + if n < 2 || sz < n { + return res + } + // 2Sum 是 base case + if n == 2 { + // 双指针那一套操作 + lo, hi := start, sz-1 + for lo < hi { + sum := nums[lo] + nums[hi] + left, right := nums[lo], nums[hi] + if sum < target { + for lo < hi && nums[lo] == left { + lo++ + } + } else if sum > target { + for lo < hi && nums[hi] == right { + hi-- + } + } else { + res = append(res, []int{left, right}) + for lo < hi && nums[lo] == left { + lo++ + } + for lo < hi && nums[hi] == right { + hi-- + } + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for i := start; i < sz; i++ { + sub := nSumTarget(nums, n-1, i+1, target-nums[i]) + for _, arr := range sub { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr = append(arr, nums[i]) + res = append(res, arr) + } + for i < sz-1 && nums[i] == nums[i+1] { + i++ + } + } + } + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + public List> threeSum(int[] nums) { + Arrays.sort(nums); + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0); + } + + /* 注意:调用这个函数之前一定要先给 nums 排序 */ + // n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + public List> nSumTarget( + int[] nums, int n, int start, int target) { + + int sz = nums.length; + List> res = new ArrayList<>(); + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n == 2) { + // 双指针那一套操作 + int lo = start, hi = sz - 1; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + int left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] == left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] == right) hi--; + } else { + res.add(new ArrayList<>(Arrays.asList(left, right))); + while (lo < hi && nums[lo] == left) lo++; + while (lo < hi && nums[hi] == right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (int i = start; i < sz; i++) { + List> + sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (List arr : sub) { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr.add(nums[i]); + res.add(arr); + } + while (i < sz - 1 && nums[i] == nums[i + 1]) i++; + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var threeSum = function(nums) { + nums.sort((a, b) => a - b); + // n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return nSumTarget(nums, 3, 0, 0); +} + +/* 注意:调用这个函数之前一定要先给 nums 排序 */ +// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 +var nSumTarget = function(nums, n, start, target) { + var sz = nums.length; + var res = []; + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n == 2) { + // 双指针那一套操作 + var lo = start, hi = sz - 1; + while (lo < hi) { + var sum = nums[lo] + nums[hi]; + var left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] == left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] == right) hi--; + } else { + res.push([left, right]); + while (lo < hi && nums[lo] == left) lo++; + while (lo < hi && nums[hi] == right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (var i = start; i < sz; i++) { + var sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (var j = 0; j < sub.length; j++) { + // (n-1)Sum 加上 nums[i] 就是 nSum + sub[j].push(nums[i]); + res.push(sub[j]); + } + while (i < sz - 1 && nums[i] == nums[i + 1]) i++; + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def threeSum(self, nums: List[int]) -> List[List[int]]: + nums.sort() + # n 为 3,从 nums[0] 开始计算和为 0 的三元组 + return self.nSumTarget(nums, 3, 0, 0) + + # 注意:调用这个函数之前一定要先给 nums 排序 + # n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + def nSumTarget(self, nums: List[int], n: int, start: int, target: int) -> List[List[int]]: + sz = len(nums) + res = [] + # 至少是 2Sum,且数组大小不应该小于 n + if n < 2 or sz < n: + return res + # 2Sum 是 base case + if n == 2: + # 双指针那一套操作 + lo, hi = start, sz - 1 + while lo < hi: + s = nums[lo] + nums[hi] + left, right = nums[lo], nums[hi] + if s < target: + while lo < hi and nums[lo] == left: + lo += 1 + elif s > target: + while lo < hi and nums[hi] == right: + hi -= 1 + else: + res.append([left, right]) + while lo < hi and nums[lo] == left: + lo += 1 + while lo < hi and nums[hi] == right: + hi -= 1 + else: + # n > 2 时,递归计算 (n-1)Sum 的结果 + for i in range(start, sz): + sub = self.nSumTarget(nums, n - 1, i + 1, target - nums[i]) + for arr in sub: + # (n-1)Sum 加上 nums[i] 就是 nSum + arr.append(nums[i]) + res.append(arr) + while i < sz - 1 and nums[i] == nums[i + 1]: + i += 1 + return res +``` + +https://leetcode.cn/problems/3sum 的多语言解法👆 + +https://leetcode.cn/problems/3sum-closest 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int threeSumClosest(vector& nums, int target) { + if (nums.size() < 3) { + return 0; + } + // 别忘了要先排序数组 + sort(nums.begin(), nums.end()); + // 记录三数之和与目标值的偏差 + int delta = INT_MAX; + for (int i = 0; i < nums.size() - 2; i++) { + // 固定 nums[i] 为三数之和中的第一个数, + // 然后对 nums[i+1..] 搜索接近 target - nums[i] 的两数之和 + int sum = nums[i] + twoSumClosest(nums, i + 1, target - nums[i]); + if (abs(delta) > abs(target - sum)) { + delta = target - sum; + } + } + return target - delta; + } + + // 在 nums[start..] 搜索最接近 target 的两数之和 + int twoSumClosest(vector& nums, int start, int target) { + int lo = start, hi = nums.size() - 1; + // 记录两数之和与目标值的偏差 + int delta = INT_MAX; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + if (abs(delta) > abs(target - sum)) { + delta = target - sum; + } + if (sum < target) { + lo++; + } else { + hi--; + } + } + return target - delta; + } +}; +``` + +```go +// by chatGPT (go) +func threeSumClosest(nums []int, target int) int { + if len(nums) < 3 { + return 0 + } + // 先排序数组 + sort.Ints(nums) + // 记录三数之和与目标值的偏差 + delta := math.MaxInt32 + for i := 0; i < len(nums)-2; i++ { + // 固定 nums[i] 为三数之和中的第一个数, + // 然后对 nums[i+1..] 搜索接近 target - nums[i] 的两数之和 + sum := nums[i] + twoSumClosest(nums, i+1, target-nums[i]) + if int(math.Abs(float64(delta))) > int(math.Abs(float64(target-sum))) { + delta = target - sum + } + } + return target - delta +} + +// 在 nums[start..] 搜索最接近 target 的两数之和 +func twoSumClosest(nums []int, start int, target int) int { + lo, hi := start, len(nums)-1 + // 记录两数之和与目标值的偏差 + delta := math.MaxInt32 + for lo < hi { + sum := nums[lo] + nums[hi] + if int(math.Abs(float64(delta))) > int(math.Abs(float64(target-sum))) { + delta = target - sum + } + if sum < target { + lo++ + } else { + hi-- + } + } + return target - delta +} +``` + +```java +// by labuladong (java) +class Solution { + public int threeSumClosest(int[] nums, int target) { + if (nums.length < 3) { + return 0; + } + // 别忘了要先排序数组 + Arrays.sort(nums); + // 记录三数之和与目标值的偏差 + int delta = Integer.MAX_VALUE; + for (int i = 0; i < nums.length - 2; i++) { + // 固定 nums[i] 为三数之和中的第一个数, + // 然后对 nums[i+1..] 搜索接近 target - nums[i] 的两数之和 + int sum = nums[i] + twoSumClosest(nums, i + 1, target - nums[i]); + if (Math.abs(delta) > Math.abs(target - sum)) { + delta = target - sum; + } + } + return target - delta; + } + + // 在 nums[start..] 搜索最接近 target 的两数之和 + int twoSumClosest(int[] nums, int start, int target) { + int lo = start, hi = nums.length - 1; + // 记录两数之和与目标值的偏差 + int delta = Integer.MAX_VALUE; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + if (Math.abs(delta) > Math.abs(target - sum)) { + delta = target - sum; + } + if (sum < target) { + lo++; + } else { + hi--; + } + } + return target - delta; + } +} +``` + +```javascript +// by chatGPT (javascript) +var threeSumClosest = function(nums, target) { + if (nums.length < 3) { + return 0; + } + // 别忘了要先排序数组 + nums.sort(function(a, b) { + return a - b; + }); + // 记录三数之和与目标值的偏差 + var delta = Number.MAX_SAFE_INTEGER; + for (var i = 0; i < nums.length - 2; i++) { + // 固定 nums[i] 为三数之和中的第一个数, + // 然后对 nums[i+1..] 搜索接近 target - nums[i] 的两数之和 + var sum = nums[i] + twoSumClosest(nums, i + 1, target - nums[i]); + if (Math.abs(delta) > Math.abs(target - sum)) { + delta = target - sum; + } + } + return target - delta; +}; + +// 在 nums[start..] 搜索最接近 target 的两数之和 +var twoSumClosest = function(nums, start, target) { + var lo = start, hi = nums.length - 1; + // 记录两数之和与目标值的偏差 + var delta = Number.MAX_SAFE_INTEGER; + while (lo < hi) { + var sum = nums[lo] + nums[hi]; + if (Math.abs(delta) > Math.abs(target - sum)) { + delta = target - sum; + } + if (sum < target) { + lo++; + } else { + hi--; + } + } + return target - delta; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def threeSumClosest(self, nums: List[int], target: int) -> int: + if len(nums) < 3: + return 0 + # 别忘了要先排序数组 + nums.sort() + # 记录三数之和与目标值的偏差 + delta = float('inf') + for i in range(len(nums) - 2): + # 固定 nums[i] 为三数之和中的第一个数, + # 然后对 nums[i+1..] 搜索接近 target - nums[i] 的两数之和 + sum_ = nums[i] + self.twoSumClosest(nums, i + 1, target - nums[i]) + if abs(delta) > abs(target - sum_): + delta = target - sum_ + return target - delta + + # 在 nums[start..] 搜索最接近 target 的两数之和 + def twoSumClosest(self, nums: List[int], start: int, target: int) -> int: + lo, hi = start, len(nums) - 1 + # 记录两数之和与目标值的偏差 + delta = float('inf') + while lo < hi: + sum_ = nums[lo] + nums[hi] + if abs(delta) > abs(target - sum_): + delta = target - sum_ + if sum_ < target: + lo += 1 + else: + hi -= 1 + return target - delta +``` + +https://leetcode.cn/problems/3sum-closest 的多语言解法👆 + +https://leetcode.cn/problems/3sum-smaller 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int threeSumSmaller(vector& nums, int target) { + if (nums.size() < 3) { + return 0; + } + // 别忘了要先排序数组 + sort(nums.begin(), nums.end()); + int res = 0; + for (int i = 0; i < nums.size() - 2; i++) { + // 固定 nums[i] 为三数之和中的第一个数, + // 然后对 nums[i+1..] 搜索小于 target - nums[i] 的两数之和个数 + res += twoSumSmaller(nums, i + 1, target - nums[i]); + } + return res; + } + + // 在 nums[start..] 搜索小于 target 的两数之和个数 + int twoSumSmaller(vector& nums, int start, int target) { + int lo = start, hi = nums.size() - 1; + int count = 0; + while (lo < hi) { + if (nums[lo] + nums[hi] < target) { + // nums[lo] 和 nums[lo+1..hi] + // 中的任一元素之和都小于 target + count += hi - lo; + lo++; + } else { + hi--; + } + } + return count; + } +}; +``` + +```go +// by chatGPT (go) +func threeSumSmaller(nums []int, target int) int { + if len(nums) < 3 { + return 0 + } + // 别忘了要先排序数组 + sort.Ints(nums) + res := 0 + for i := 0; i < len(nums) - 2; i++ { + // 固定 nums[i] 为三数之和中的第一个数, + // 然后对 nums[i+1..] 搜索小于 target - nums[i] 的两数之和个数 + res += twoSumSmaller(nums, i + 1, target - nums[i]) + } + return res +} + +// 在 nums[start..] 搜索小于 target 的两数之和个数 +func twoSumSmaller(nums []int, start int, target int) int { + lo, hi := start, len(nums) - 1 + count := 0 + for lo < hi { + if nums[lo] + nums[hi] < target { + // nums[lo] 和 nums[lo+1..hi] + // 中的任一元素之和都小于 target + count += hi - lo + lo++ + } else { + hi-- + } + } + return count +} +``` + +```java +// by labuladong (java) +class Solution { + public int threeSumSmaller(int[] nums, int target) { + if (nums.length < 3) { + return 0; + } + // 别忘了要先排序数组 + Arrays.sort(nums); + int res = 0; + for (int i = 0; i < nums.length - 2; i++) { + // 固定 nums[i] 为三数之和中的第一个数, + // 然后对 nums[i+1..] 搜索小于 target - nums[i] 的两数之和个数 + res += twoSumSmaller(nums, i + 1, target - nums[i]); + } + return res; + } + + // 在 nums[start..] 搜索小于 target 的两数之和个数 + int twoSumSmaller(int[] nums, int start, int target) { + int lo = start, hi = nums.length - 1; + int count = 0; + while (lo < hi) { + if (nums[lo] + nums[hi] < target) { + // nums[lo] 和 nums[lo+1..hi] + // 中的任一元素之和都小于 target + count += hi - lo; + lo++; + } else { + hi--; + } + } + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +var threeSumSmaller = function(nums, target) { + // 如果数组长度小于3,返回0 + if(nums.length < 3) { + return 0; + } + // 将数组升序排序 + nums.sort(function(a,b) {return a - b;}); + // 定义变量res,初始化为0 + var res = 0; + // 枚举第一个元素 + for (var i = 0; i < nums.length - 2; i++) { + // 对第一个元素之后的元素搜索两数之和小于target-nums[i]的个数 + res += twoSumSmaller(nums, i + 1, target - nums[i]); + } + // 返回答案 + return res; + + // 搜索nums[start..]内两数之和小于target的个数 + function twoSumSmaller(nums, start, target) { + var lo = start, hi = nums.length - 1; + var count = 0; + while (lo < hi) { + // 如果nums[lo] + nums[hi] < target,这时nums[lo]和nums[lo + 1..hi]中任意一个数与nums[hi]相加都会小于target + if (nums[lo] + nums[hi] < target) { + // 计算加入nums[lo]时小于target的两数之和的个数,并将lo移动一位 + count += hi - lo; + lo++; + } else { + // 如果nums[lo] + nums[hi] >= target,则将hi往前一位 + hi--; + } + } + // 返回小于target的两数之和的个数 + return count; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def threeSumSmaller(self, nums: List[int], target: int) -> int: + if len(nums) < 3: + return 0 + nums.sort() + res = 0 + for i in range(len(nums) - 2): + # 固定 nums[i] 为三数之和中的第一个数, + # 然后对 nums[i+1..] 搜索小于 target - nums[i] 的两数之和个数 + res += self.twoSumSmaller(nums, i + 1, target - nums[i]) + return res + + def twoSumSmaller(self, nums: List[int], start: int, target: int) -> int: + lo = start + hi = len(nums) - 1 + count = 0 + while lo < hi: + if nums[lo] + nums[hi] < target: + # nums[lo] 和 nums[lo+1..hi] + # 中的任一元素之和都小于 target + count += hi - lo + lo += 1 + else: + hi -= 1 + return count +``` + +https://leetcode.cn/problems/3sum-smaller 的多语言解法👆 + +https://leetcode.cn/problems/3u1WK4 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + ListNode *p1 = headA, *p2 = headB; + while (p1 != p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 == nullptr) p1 = headB; + else p1 = p1->next; + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 == nullptr) p2 = headA; + else p2 = p2->next; + } + return p1; + } +}; +``` + +```go +// by chatGPT (go) +func getIntersectionNode(headA, headB *ListNode) *ListNode { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + p1, p2 := headA, headB + for p1 != p2 { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if p1 == nil { + p1 = headB + } else { + p1 = p1.Next + } + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if p2 == nil { + p2 = headA + } else { + p2 = p2.Next + } + } + return p1 +} +``` + +```java +// by labuladong (java) +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + ListNode p1 = headA, p2 = headB; + while (p1 != p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 == null) p1 = headB; + else p1 = p1.next; + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 == null) p2 = headA; + else p2 = p2.next; + } + return p1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var getIntersectionNode = function(headA, headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + let p1 = headA, p2 = headB; + while (p1 !== p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 === null) p1 = headB; + else p1 = p1.next; + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 === null) p2 = headA; + else p2 = p2.next; + } + return p1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: + # p1 指向 A 链表头结点,p2 指向 B 链表头结点 + p1, p2 = headA, headB + while p1 != p2: + # p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if p1 is None: + p1 = headB + else: + p1 = p1.next + # p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if p2 is None: + p2 = headA + else: + p2 = p2.next + return p1 +``` + +https://leetcode.cn/problems/3u1WK4 的多语言解法👆 + +https://leetcode.cn/problems/4sum 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector> fourSum(vector& nums, int target) { + sort(nums.begin(), nums.end()); + // n 为 4,从 nums[0] 开始计算和为 target 的四元组 + return nSumTarget(nums, 4, 0, target); + } + + /* 注意:调用这个函数之前一定要先给 nums 排序 */ + // n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + vector> nSumTarget( + vector& nums, int n, int start, int target) { + + int sz = nums.size(); + vector> res; + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n == 2) { + // 双指针那一套操作 + int lo = start, hi = sz - 1; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + int left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] == left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] == right) hi--; + } else { + res.push_back({left, right}); + while (lo < hi && nums[lo] == left) lo++; + while (lo < hi && nums[hi] == right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (int i = start; i < sz; i++) { + vector> + sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (vector& arr : sub) { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr.push_back(nums[i]); + res.push_back(arr); + } + while (i < sz - 1 && nums[i] == nums[i + 1]) i++; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func fourSum(nums []int, target int) [][]int { + sort.Ints(nums) + // n 为 4,从 nums[0] 开始计算和为 target 的四元组 + return nSumTarget(nums, 4, 0, target) +} + +/* 注意:调用这个函数之前一定要先给 nums 排序 */ +// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 +func nSumTarget(nums []int, n, start, target int) [][]int { + sz := len(nums) + res := [][]int{} + // 至少是 2Sum,且数组大小不应该小于 n + if n < 2 || sz < n { + return res + } + // 2Sum 是 base case + if n == 2 { + // 双指针那一套操作 + lo, hi := start, sz-1 + for lo < hi { + sum := nums[lo] + nums[hi] + left, right := nums[lo], nums[hi] + if sum < target { + for lo < hi && nums[lo] == left { + lo++ + } + } else if sum > target { + for lo < hi && nums[hi] == right { + hi-- + } + } else { + res = append(res, []int{left, right}) + for lo < hi && nums[lo] == left { + lo++ + } + for lo < hi && nums[hi] == right { + hi-- + } + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for i := start; i < sz; i++ { + sub := nSumTarget(nums, n-1, i+1, target-nums[i]) + for _, arr := range sub { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr = append(arr, nums[i]) + res = append(res, arr) + } + for i < sz-1 && nums[i] == nums[i+1] { + i++ + } + } + } + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + public List> fourSum(int[] nums, int target) { + Arrays.sort(nums); + // n 为 4,从 nums[0] 开始计算和为 target 的四元组 + return nSumTarget(nums, 4, 0, target); + } + + /* 注意:调用这个函数之前一定要先给 nums 排序 */ + // n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + private List> nSumTarget(int[] nums, int n, int start, int target) { + int sz = nums.length; + List> res = new ArrayList<>(); + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n == 2) { + // 双指针那一套操作 + int lo = start, hi = sz - 1; + while (lo < hi) { + int sum = nums[lo] + nums[hi]; + int left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] == left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] == right) hi--; + } else { + res.add(new ArrayList<>(Arrays.asList(left, right))); + while (lo < hi && nums[lo] == left) lo++; + while (lo < hi && nums[hi] == right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (int i = start; i < sz; i++) { + List> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (List arr : sub) { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr.add(nums[i]); + res.add(arr); + } + while (i < sz - 1 && nums[i] == nums[i + 1]) i++; + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var fourSum = function(nums, target) { + nums.sort((a, b) => a - b); + // n 为 4,从 nums[0] 开始计算和为 target 的四元组 + return nSumTarget(nums, 4, 0, target); +}; + +/* 注意:调用这个函数之前一定要先给 nums 排序 */ +// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 +var nSumTarget = function(nums, n, start, target) { + var sz = nums.length; + var res = []; + // 至少是 2Sum,且数组大小不应该小于 n + if (n < 2 || sz < n) return res; + // 2Sum 是 base case + if (n === 2) { + // 双指针那一套操作 + var lo = start, hi = sz - 1; + while (lo < hi) { + var sum = nums[lo] + nums[hi]; + var left = nums[lo], right = nums[hi]; + if (sum < target) { + while (lo < hi && nums[lo] === left) lo++; + } else if (sum > target) { + while (lo < hi && nums[hi] === right) hi--; + } else { + res.push([left, right]); + while (lo < hi && nums[lo] === left) lo++; + while (lo < hi && nums[hi] === right) hi--; + } + } + } else { + // n > 2 时,递归计算 (n-1)Sum 的结果 + for (var i = start; i < sz; i++) { + var sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]); + for (var arr of sub) { + // (n-1)Sum 加上 nums[i] 就是 nSum + arr.push(nums[i]); + res.push(arr); + } + while (i < sz - 1 && nums[i] === nums[i + 1]) i++; + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def fourSum(self, nums: List[int], target: int) -> List[List[int]]: + nums.sort() + # n 为 4,从 nums[0] 开始计算和为 target 的四元组 + return self.nSumTarget(nums, 4, 0, target) + + # 注意:调用这个函数之前一定要先给 nums 排序 + # n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和 + def nSumTarget(self, nums: List[int], n: int, start: int, target: int) -> List[List[int]]: + sz = len(nums) + res = [] + # 至少是 2Sum,且数组大小不应该小于 n + if n < 2 or sz < n: + return res + # 2Sum 是 base case + if n == 2: + # 双指针那一套操作 + lo, hi = start, sz - 1 + while lo < hi: + s = nums[lo] + nums[hi] + left, right = nums[lo], nums[hi] + if s < target: + while lo < hi and nums[lo] == left: + lo += 1 + elif s > target: + while lo < hi and nums[hi] == right: + hi -= 1 + else: + res.append([left, right]) + while lo < hi and nums[lo] == left: + lo += 1 + while lo < hi and nums[hi] == right: + hi -= 1 + else: + # n > 2 时,递归计算 (n-1)Sum 的结果 + for i in range(start, sz): + sub = self.nSumTarget(nums, n - 1, i + 1, target - nums[i]) + for arr in sub: + # (n-1)Sum 加上 nums[i] 就是 nSum + arr.append(nums[i]) + res.append(arr) + while i < sz - 1 and nums[i] == nums[i + 1]: + i += 1 + return res +``` + +https://leetcode.cn/problems/4sum 的多语言解法👆 + +https://leetcode.cn/problems/8Zf90G 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int evalRPN(vector& tokens) { + stack stk; + for (const auto& token : tokens) { + if (string("+-*/").find(token) != string::npos) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + int a = stk.top(); + stk.pop(); + int b = stk.top(); + stk.pop(); + switch (token[0]) { + case '+': + stk.push(b + a); + break; + case '*': + stk.push(b * a); + break; + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case '-': + stk.push(b - a); + break; + case '/': + stk.push(b / a); + break; + } + } else { + // 是个数字,直接入栈即可 + stk.push(stoi(token)); + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk.top(); + } +}; +``` + +```go +// by chatGPT (go) +func evalRPN(tokens []string) int { + stk := make([]int, 0) + for _, token := range tokens { + if strings.Contains("+-*/", token) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + a, b := stk[len(stk)-1], stk[len(stk)-2] + stk = stk[:len(stk)-2] + switch token { + case "+": + stk = append(stk, a+b) + case "*": + stk = append(stk, a*b) + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case "-": + stk = append(stk, b-a) + case "/": + stk = append(stk, b/a) + } + } else { + // 是个数字,直接入栈即可 + num, _ := strconv.Atoi(token) + stk = append(stk, num) + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk[0] +} +``` + +```java +// by labuladong (java) +class Solution { + public int evalRPN(String[] tokens) { + Stack stk = new Stack<>(); + for (String token : tokens) { + if ("+-*/".contains(token)) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + int a = stk.pop(), b = stk.pop(); + switch (token) { + case "+": + stk.push(a + b); + break; + case "*": + stk.push(a * b); + break; + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case "-": + stk.push(b - a); + break; + case "/": + stk.push(b / a); + break; + } + } else { + // 是个数字,直接入栈即可 + stk.push(Integer.parseInt(token)); + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk.pop(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var evalRPN = function(tokens) { + let stk = []; + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + if ("+-*/".includes(token)) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + let a = stk.pop(), b = stk.pop(); + switch (token) { + case "+": + stk.push(a + b); + break; + case "*": + stk.push(a * b); + break; + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case "-": + stk.push(b - a); + break; + case "/": + stk.push(parseInt(b / a)); + break; + } + } else { + // 是个数字,直接入栈即可 + stk.push(parseInt(token)); + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk.pop(); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def evalRPN(self, tokens: List[str]) -> int: + stk = [] + for token in tokens: + if token in "+-*/": + # 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + a = stk.pop() + b = stk.pop() + if token == "+": + stk.append(a + b) + elif token == "-": + stk.append(b - a) + elif token == "*": + stk.append(a * b) + else: # token == "/" + stk.append(int(b / a)) + else: + # 是个数字,直接入栈即可 + stk.append(int(token)) + # 最后栈中剩下一个数字,即是计算结果 + return stk.pop() +``` + +https://leetcode.cn/problems/8Zf90G 的多语言解法👆 + +https://leetcode.cn/problems/B1IidL 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int peakIndexInMountainArray(vector& nums) { + // 取两端都闭的二分搜索 + int left = 0, right = nums.size() - 1; + // 因为题目必然有解,所以设置 left == right 为结束条件 + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] > nums[mid + 1]) { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid; + } else { + // mid 右侧有一个峰值 + left = mid + 1; + } + } + return left; + } +}; +``` + +```go +// by chatGPT (go) +func peakIndexInMountainArray(nums []int) int { + // 取两端都闭的二分搜索 + left, right := 0, len(nums)-1 + // 因为题目必然有解,所以设置 left == right 为结束条件 + for left < right { + mid := left + (right-left)/2 + if nums[mid] > nums[mid+1] { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid + } else { + // mid 右侧有一个峰值 + left = mid + 1 + } + } + return left +} +``` + +```java +// by labuladong (java) +class Solution { + public int peakIndexInMountainArray(int[] nums) { + // 取两端都闭的二分搜索 + int left = 0, right = nums.length - 1; + // 因为题目必然有解,所以设置 left == right 为结束条件 + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] > nums[mid + 1]) { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid; + } else { + // mid 右侧有一个峰值 + left = mid + 1; + } + } + return left; + } +} +``` + +```javascript +// by chatGPT (javascript) +var peakIndexInMountainArray = function(nums) { + // 取两端都闭的二分搜索 + let left = 0, right = nums.length - 1; + // 因为题目必然有解,所以设置 left == right 为结束条件 + while (left < right) { + let mid = left + Math.floor((right - left) / 2); + if (nums[mid] > nums[mid + 1]) { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid; + } else { + // mid 右侧有一个峰值 + left = mid + 1; + } + } + return left; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def peakIndexInMountainArray(self, nums: List[int]) -> int: + # 取两端都闭的二分搜索 + left, right = 0, len(nums) - 1 + # 因为题目必然有解,所以设置 left == right 为结束条件 + while left < right: + mid = left + (right - left) // 2 + if nums[mid] > nums[mid + 1]: + # mid 本身就是峰值或其左侧有一个峰值 + right = mid + else: + # mid 右侧有一个峰值 + left = mid + 1 + return left +``` + +https://leetcode.cn/problems/B1IidL 的多语言解法👆 + +https://leetcode.cn/problems/FortPu 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class RandomizedSet { + public: + // 存储元素的值 + vector nums; + // 记录每个元素对应在 nums 中的索引 + unordered_map valToIndex; + + bool insert(int val) { + // 若 val 已存在,不用再插入 + if (valToIndex.count(val)) { + return false; + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + valToIndex[val] = nums.size(); + nums.push_back(val); + return true; + } + + bool remove(int val) { + // 若 val 不存在,不用再删除 + if (!valToIndex.count(val)) { + return false; + } + // 先拿到 val 的索引 + int index = valToIndex[val]; + // 将最后一个元素对应的索引修改为 index + valToIndex[nums.back()] = index; + // 交换 val 和最后一个元素 + swap(nums[index], nums.back()); + // 在数组中删除元素 val + nums.pop_back(); + // 删除元素 val 对应的索引 + valToIndex.erase(val); + return true; + } + + int getRandom() { + // 随机获取 nums 中的一个元素 + return nums[rand() % nums.size()]; + } +}; +``` + +```go +// by chatGPT (go) +// 定义结构体 +type RandomizedSet struct { + // 存储元素的值 + nums []int + // 记录每个元素对应在 nums 中的索引 + valToIndex map[int]int +} + +// 插入操作 +func (r *RandomizedSet) insert(val int) bool { + // 若 val 已存在,不用再插入 + if _, ok := r.valToIndex[val]; ok { + return false + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + r.valToIndex[val] = len(r.nums) + r.nums = append(r.nums, val) + return true +} + +// 删除操作 +func (r *RandomizedSet) remove(val int) bool { + // 若 val 不存在,不用再删除 + if _, ok := r.valToIndex[val]; !ok { + return false + } + // 先拿到 val 的索引 + index := r.valToIndex[val] + // 将最后一个元素对应的索引修改为 index + r.valToIndex[r.nums[len(r.nums)-1]] = index + // 交换 val 和最后一个元素 + r.nums[index], r.nums[len(r.nums)-1] = r.nums[len(r.nums)-1], r.nums[index] + // 在数组中删除元素 val + r.nums = r.nums[:len(r.nums)-1] + // 删除元素 val 对应的索引 + delete(r.valToIndex, val) + return true +} + +// 随机获取元素 +func (r *RandomizedSet) getRandom() int { + // 随机获取 nums 中的一个元素 + return r.nums[rand.Intn(len(r.nums))] +} +``` + +```java +// by chatGPT (java) +class RandomizedSet { + // 存储元素的值 + List nums; + // 记录每个元素对应在 nums 中的索引 + Map valToIndex; + + public boolean insert(int val) { + // 若 val 已存在,不用再插入 + if (valToIndex.containsKey(val)) { + return false; + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + valToIndex.put(val, nums.size()); + nums.add(val); + return true; + } + + public boolean remove(int val) { + // 若 val 不存在,不用再删除 + if (!valToIndex.containsKey(val)) { + return false; + } + // 先拿到 val 的索引 + int index = valToIndex.get(val); + // 将最后一个元素对应的索引修改为 index + valToIndex.put(nums.get(nums.size()-1), index); + // 交换 val 和最后一个元素 + Collections.swap(nums, index, nums.size()-1); + // 在数组中删除元素 val + nums.remove(nums.size()-1); + // 删除元素 val 对应的索引 + valToIndex.remove(val); + return true; + } + + public int getRandom() { + // 随机获取 nums 中的一个元素 + return nums.get((int)(Math.random() * nums.size())); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * Initialize your data structure here. + */ +var RandomizedSet = function() { + // 存储元素的值 + this.nums = []; + // 记录每个元素对应在 nums 中的索引 + this.valToIndex = new Map(); +}; + +/** + * Inserts a value to the set. Returns true if the set did not already contain the specified element. + * @param {number} val + * @return {boolean} + */ +RandomizedSet.prototype.insert = function(val) { + // 若 val 已存在,不用再插入 + if (this.valToIndex.has(val)) { + return false; + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + this.valToIndex.set(val, this.nums.length); + this.nums.push(val); + return true; +}; + +/** + * Removes a value from the set. Returns true if the set contained the specified element. + * @param {number} val + * @return {boolean} + */ +RandomizedSet.prototype.remove = function(val) { + // 若 val 不存在,不用再删除 + if (!this.valToIndex.has(val)) { + return false; + } + // 先拿到 val 的索引 + const index = this.valToIndex.get(val); + // 将最后一个元素对应的索引修改为 index + this.valToIndex.set(this.nums[this.nums.length - 1], index); + // 交换 val 和最后一个元素 + [this.nums[index], this.nums[this.nums.length - 1]] = [this.nums[this.nums.length - 1], this.nums[index]]; + // 在数组中删除元素 val + this.nums.pop(); + // 删除元素 val 对应的索引 + this.valToIndex.delete(val); + return true; +}; + +/** + * Get a random element from the set. + * @return {number} + */ +RandomizedSet.prototype.getRandom = function() { + // 随机获取 nums 中的一个元素 + return this.nums[Math.floor(Math.random() * this.nums.length)]; +}; +``` + +```python +# by chatGPT (python) +class RandomizedSet: + def __init__(self): + # 存储元素的值 + self.nums = [] + # 记录每个元素对应在 nums 中的索引 + self.valToIndex = {} + + def insert(self, val: int) -> bool: + # 若 val 已存在,不用再插入 + if val in self.valToIndex: + return False + # 若 val 不存在,插入到 nums 尾部, + # 并记录 val 对应的索引值 + self.valToIndex[val] = len(self.nums) + self.nums.append(val) + return True + + def remove(self, val: int) -> bool: + # 若 val 不存在,不用再删除 + if val not in self.valToIndex: + return False + # 先拿到 val 的索引 + index = self.valToIndex[val] + # 将最后一个元素对应的索引修改为 index + self.valToIndex[self.nums[-1]] = index + # 交换 val 和最后一个元素 + self.nums[index], self.nums[-1] = self.nums[-1], self.nums[index] + # 在数组中删除元素 val + self.nums.pop() + # 删除元素 val 对应的索引 + self.valToIndex.pop(val) + return True + + def getRandom(self) -> int: + # 随机获取 nums 中的一个元素 + return self.nums[random.randint(0, len(self.nums) - 1)] +``` + +https://leetcode.cn/problems/FortPu 的多语言解法👆 + +https://leetcode.cn/problems/Gu0c2T 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + // 备忘录 + vector memo; + // dp 函数 + int dp(vector& nums, int start) { + if (start >= nums.size()) { + return 0; + } + // 避免重复计算 + if (memo[start] != -1) { + return memo[start]; + } + int res = max(dp(nums, start + 1), + nums[start] + dp(nums, start + 2)); + // 记入备忘录 + memo[start] = res; + return res; + } + +public: + // 主函数 + int rob(vector& nums) { + // 初始化备忘录 + memo.resize(nums.size(), -1); + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0); + } +}; +``` + +```go +// by chatGPT (go) +import ( + "fmt" +) + +func rob(nums []int) int { + // 初始化备忘录 + memo := make([]int, len(nums)) + for i := 0; i < len(memo); i++ { + memo[i] = -1 + } + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0, memo) +} + +// 返回 dp[start..] 能抢到的最大值 +func dp(nums []int, start int, memo []int) int { + if start >= len(nums) { + return 0 + } + // 避免重复计算 + if memo[start] != -1 { + return memo[start] + } + + res := max(dp(nums, start+1, memo), nums[start]+dp(nums, start+2, memo)) + // 记入备忘录 + memo[start] = res + return res +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + // 备忘录 + private int[] memo; + // 主函数 + public int rob(int[] nums) { + // 初始化备忘录 + memo = new int[nums.length]; + Arrays.fill(memo, -1); + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0); + } + + // 返回 dp[start..] 能抢到的最大值 + private int dp(int[] nums, int start) { + if (start >= nums.length) { + return 0; + } + // 避免重复计算 + if (memo[start] != -1) return memo[start]; + + int res = Math.max(dp(nums, start + 1), + nums[start] + dp(nums, start + 2)); + // 记入备忘录 + memo[start] = res; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var rob = function(nums) { + // 备忘录 + const memo = new Array(nums.length).fill(-1); + + // 返回 dp[start..] 能抢到的最大值 + const dp = (start) => { + if (start >= nums.length) { + return 0; + } + // 避免重复计算 + if (memo[start] != -1) return memo[start]; + + const res = Math.max(dp(start + 1), nums[start] + dp(start + 2)); + // 记入备忘录 + memo[start] = res; + return res; + } + + // 强盗从第 0 间房子开始抢劫 + return dp(0); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + # 备忘录 + self.memo = None + + # 主函数 + def rob(self, nums: List[int]) -> int: + # 初始化备忘录 + self.memo = [-1] * len(nums) + # 强盗从第 0 间房子开始抢劫 + return self.dp(nums, 0) + + # 返回 dp[start..] 能抢到的最大值 + def dp(self, nums: List[int], start: int) -> int: + if start >= len(nums): + return 0 + # 避免重复计算 + if self.memo[start] != -1: + return self.memo[start] + res = max(self.dp(nums, start + 1), + nums[start] + self.dp(nums, start + 2)) + # 记入备忘录 + self.memo[start] = res + return res +``` + +https://leetcode.cn/problems/Gu0c2T 的多语言解法👆 + +https://leetcode.cn/problems/IDBivT 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector generateParenthesis(int n) { + if (n == 0) return {}; + // 记录所有合法的括号组合 + vector res; + // 回溯过程中的路径 + string track; + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, track, res); + return res; + } + + // 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个 + void backtrack(int left, int right, + string& track, vector& res) { + // 若左括号剩下的多,说明不合法 + if (right < left) return; + // 数量小于 0 肯定是不合法的 + if (left < 0 || right < 0) return; + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if (left == 0 && right == 0) { + res.push_back(track); + return; + } + + // 尝试放一个左括号 + track.push_back('('); // 选择 + backtrack(left - 1, right, track, res); + track.pop_back(); // 撤消选择 + + // 尝试放一个右括号 + track.push_back(')'); // 选择 + backtrack(left, right - 1, track, res); + track.pop_back(); // 撤消选择 + } +} +``` + +```go +// by chatGPT (go) +func generateParenthesis(n int) []string { + if n == 0 { + return []string{} + } + // 记录所有合法的括号组合 + var res []string + // 回溯过程中的路径 + var track string + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, &track, &res) + return res +} + +// 可用的左括号数量为 left 个,可用的右括号数量为 right 个 +func backtrack(left, right int, track *string, res *[]string) { + // 若左括号剩下的多,说明不合法 + if right < left { + return + } + // 数量小于 0 肯定是不合法的 + if left < 0 || right < 0 { + return + } + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if left == 0 && right == 0 { + *res = append(*res, *track) + return + } + + // 尝试放一个左括号 + *track += "(" // 选择 + backtrack(left-1, right, track, res) + *track = (*track)[:len(*track)-1] // 撤消选择 + + // 尝试放一个右括号 + *track += ")" // 选择 + backtrack(left, right-1, track, res) + *track = (*track)[:len(*track)-1] // 撤消选择 +} +``` + +```java +// by chatGPT (java) +class Solution { + public List generateParenthesis(int n) { + if (n == 0) return new ArrayList<>(); + // 记录所有合法的括号组合 + List res = new ArrayList<>(); + // 回溯过程中的路径 + StringBuilder track = new StringBuilder(); + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, track, res); + return res; + } + + // 可用的左括号数量为 left 个,可用的右括号数量为 right 个 + void backtrack(int left, int right, + StringBuilder track, List res) { + // 若左括号剩下的多,说明不合法 + if (right < left) return; + // 数量小于 0 肯定是不合法的 + if (left < 0 || right < 0) return; + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if (left == 0 && right == 0) { + res.add(track.toString()); + return; + } + + // 尝试放一个左括号 + track.append('('); // 选择 + backtrack(left - 1, right, track, res); + track.deleteCharAt(track.length() - 1); // 撤消选择 + + // 尝试放一个右括号 + track.append(')'); // 选择 + backtrack(left, right - 1, track, res); + track.deleteCharAt(track.length() - 1); // 撤消选择 + } +} +``` + +```javascript +// by chatGPT (javascript) +var generateParenthesis = function(n) { + if (n === 0) return []; + // 记录所有合法的括号组合 + var res = []; + // 回溯过程中的路径 + var track = ""; + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, track, res); + return res; +}; + +// 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个 +var backtrack = function(left, right, track, res) { + // 若左括号剩下的多,说明不合法 + if (right < left) return; + // 数量小于 0 肯定是不合法的 + if (left < 0 || right < 0) return; + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if (left === 0 && right === 0) { + res.push(track); + return; + } + + // 尝试放一个左括号 + track += '('; // 选择 + backtrack(left - 1, right, track, res); + track = track.substring(0, track.length - 1); // 撤消选择 + + // 尝试放一个右括号 + track += ')'; // 选择 + backtrack(left, right - 1, track, res); + track = track.substring(0, track.length - 1); // 撤消选择 +}; +``` + +```python +# by chatGPT (python) +class Solution: + def generateParenthesis(self, n: int) -> List[str]: + if n == 0: + return [] + # 记录所有合法的括号组合 + res = [] + # 回溯过程中的路径 + track = "" + # 可用的左括号和右括号数量初始化为 n + self.backtrack(n, n, track, res) + return res + + # 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个 + def backtrack(self, left: int, right: int, track: str, res: List[str]) -> None: + # 若右括号剩下的多,说明不合法 + if right < left: + return + # 数量小于 0 肯定是不合法的 + if left < 0 or right < 0: + return + # 当所有括号都恰好用完时,得到一个合法的括号组合 + if left == 0 and right == 0: + res.append(track) + return + + # 尝试放一个左括号 + track += '(' # 选择 + self.backtrack(left - 1, right, track, res) + track = track[:-1] # 撤消选择 + + # 尝试放一个右括号 + track += ')' # 选择 + self.backtrack(left, right - 1, track, res) + track = track[:-1] # 撤消选择 +``` + +https://leetcode.cn/problems/IDBivT 的多语言解法👆 + +https://leetcode.cn/problems/M1oyTv 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + string minWindow(string s, string t) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + // 记录最小覆盖子串的起始索引及长度 + int start = 0, len = INT_MAX; + /** + ![](../pictures/slidingwindow/1.png) + */ + while (right < s.size()) { + // c 是将移入窗口的字符 + char c = s[right]; + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + if (need.count(c)) { + window[c]++; + if (window[c] == need[c]) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (valid == need.size()) { + /** + ![](../pictures/slidingwindow/2.png) + */ + // 在这里更新最小覆盖子串 + if (right - left < len) { + start = left; + len = right - left; + } + // d 是将移出窗口的字符 + char d = s[left]; + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + if (need.count(d)) { + if (window[d] == need[d]) + valid--; + window[d]--; + } + } + /** + ![](../pictures/slidingwindow/4.png) + */ + } + // 返回最小覆盖子串 + return len == INT_MAX ? + "" : s.substr(start, len); + } +}; +``` + +```go +// by chatGPT (go) +func minWindow(s string, t string) string { + need := make(map[byte]int) + window := make(map[byte]int) + for i := 0; i < len(t); i++ { + need[t[i]]++ + } + + left, right, valid := 0, 0, 0 + // 记录最小覆盖子串的起始索引及长度 + start, len := 0, math.MaxInt32 + /** + ![](../pictures/slidingwindow/1.png) + */ + for right < len(s) { + // c 是将移入窗口的字符 + c := s[right] + // 右移窗口 + right++ + // 进行窗口内数据的一系列更新 + if _, ok := need[c]; ok { + window[c]++ + if window[c] == need[c] { + valid++ + } + } + + // 判断左侧窗口是否要收缩 + for valid == len(need) { + /** + ![](../pictures/slidingwindow/2.png) + */ + // 在这里更新最小覆盖子串 + if right-left < len { + start = left + len = right - left + } + + // d 是将移出窗口的字符 + d := s[left] + // 左移窗口 + left++ + // 进行窗口内数据的一系列更新 + if _, ok := need[d]; ok { + if window[d] == need[d] { + valid-- + } + window[d]-- + } + } + /** + ![](../pictures/slidingwindow/4.png) + */ + } + // 返回最小覆盖子串 + if len == math.MaxInt32 { + return "" + } + return s[start : start+len] +} +``` + +```java +// by chatGPT (java) +class Solution { + public String minWindow(String s, String t) { + HashMap need = new HashMap<>(); + HashMap window = new HashMap<>(); + for (char c : t.toCharArray()) { + need.put(c, need.getOrDefault(c, 0) + 1); + } + int left = 0, right = 0; + int valid = 0; + // 记录最小覆盖子串的起始索引及长度 + int start = 0, len = Integer.MAX_VALUE; + /** + ![](../pictures/slidingwindow/1.png) + */ + while (right < s.length()) { + // c 是将移入窗口的字符 + char c = s.charAt(right); + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) { + valid++; + } + } + + // 判断左侧窗口是否要收缩 + while (valid == need.size()) { + /** + ![](../pictures/slidingwindow/2.png) + */ + // 在这里更新最小覆盖子串 + if (right - left < len) { + start = left; + len = right - left; + } + // d 是将移出窗口的字符 + char d = s.charAt(left); + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) { + valid--; + } + window.put(d, window.get(d) - 1); + } + } + /** + ![](../pictures/slidingwindow/4.png) + */ + } + // 返回最小覆盖子串 + return len == Integer.MAX_VALUE ? + "" : s.substring(start, start + len); + } +} +``` + +```javascript +// by chatGPT (javascript) +var minWindow = function(s, t) { + var need = new Map(); + var window = new Map(); + for (var c of t) { + need.set(c, need.has(c) ? need.get(c) + 1 : 1); + } + + var left = 0, right = 0; + var valid = 0; + // 记录最小覆盖子串的起始索引及长度 + var start = 0, len = Number.MAX_SAFE_INTEGER; + /** + ![](../pictures/slidingwindow/1.png) + */ + while (right < s.length) { + // c 是将移入窗口的字符 + var c = s[right]; + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + if (need.has(c)) { + window.set(c, window.has(c) ? window.get(c) + 1 : 1); + if (window.get(c) === need.get(c)) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (valid === need.size) { + /** + ![](../pictures/slidingwindow/2.png) + */ + // 在这里更新最小覆盖子串 + if (right - left < len) { + start = left; + len = right - left; + } + // d 是将移出窗口的字符 + var d = s[left]; + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + if (need.has(d)) { + if (window.get(d) === need.get(d)) + valid--; + window.set(d, window.get(d) - 1); + } + } + /** + ![](../pictures/slidingwindow/4.png) + */ + } + // 返回最小覆盖子串 + return len === Number.MAX_SAFE_INTEGER ? + "" : s.substring(start, start + len); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minWindow(self, s: str, t: str) -> str: + from collections import defaultdict + need, window = defaultdict(int), defaultdict(int) + for c in t: + need[c] += 1 + + left, right = 0, 0 + valid = 0 + # 记录最小覆盖子串的起始索引及长度 + start, length = 0, float('inf') + # ![](../pictures/slidingwindow/1.png) + while right < len(s): + # c 是将移入窗口的字符 + c = s[right] + # 右移窗口 + right += 1 + # 进行窗口内数据的一系列更新 + if c in need: + window[c] += 1 + if window[c] == need[c]: + valid += 1 + + # 判断左侧窗口是否要收缩 + while valid == len(need): + # ![](../pictures/slidingwindow/2.png) + # 在这里更新最小覆盖子串 + if right - left < length: + start = left + length = right - left + # d 是将移出窗口的字符 + d = s[left] + # 左移窗口 + left += 1 + # 进行窗口内数据的一系列更新 + if d in need: + if window[d] == need[d]: + valid -= 1 + window[d] -= 1 + # ![](../pictures/slidingwindow/4.png) + # 返回最小覆盖子串 + return '' if length == float('inf') else s[start:start+length] +``` + +https://leetcode.cn/problems/M1oyTv 的多语言解法👆 + +https://leetcode.cn/problems/MPnaiL 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + + // 判断 s 中是否存在 t 的排列 + bool checkInclusion(string t, string s) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + while (right < s.size()) { + char c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + if (need.count(c)) { + window[c]++; + if (window[c] == need[c]) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (right - left >= t.size()) { + // 在这里判断是否找到了合法的子串 + if (valid == need.size()) + return true; + char d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + if (need.count(d)) { + if (window[d] == need[d]) + valid--; + window[d]--; + } + } + } + // 未找到符合条件的子串 + return false; + } +}; +``` + +```go +// by chatGPT (go) +func checkInclusion(t string, s string) bool { + need := make(map[byte]int) + window := make(map[byte]int) + for i := range t { + need[t[i]]++ + } + + left, right := 0, 0 + valid := 0 + for right < len(s) { + c := s[right] + right++ + // 进行窗口内数据的一系列更新 + if _, ok := need[c]; ok { + window[c]++ + if window[c] == need[c] { + valid++ + } + } + + // 判断左侧窗口是否要收缩 + for right-left >= len(t) { + // 在这里判断是否找到了合法的子串 + if valid == len(need) { + return true + } + d := s[left] + left++ + // 进行窗口内数据的一系列更新 + if _, ok := need[d]; ok { + if window[d] == need[d] { + valid-- + } + window[d]-- + } + } + } + // 未找到符合条件的子串 + return false +} +``` + +```java +// by chatGPT (java) +class Solution { + public boolean checkInclusion(String t, String s) { + Map need = new HashMap<>(); + Map window = new HashMap<>(); + for (char c : t.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1); + + int left = 0, right = 0; + int valid = 0; + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (right - left >= t.length()) { + // 在这里判断是否找到了合法的子串 + if (valid == need.size()) + return true; + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) + valid--; + window.put(d, window.get(d) - 1); + } + } + } + // 未找到符合条件的子串 + return false; + } +} +``` + +```javascript +// by chatGPT (javascript) +var checkInclusion = function(t, s) { + const need = new Map(); + const window = new Map(); + for (let c of t) { + need.set(c, (need.get(c) || 0) + 1); + } + + let left = 0, right = 0; + let valid = 0; + while (right < s.length) { + const c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + if (need.has(c)) { + window.set(c, (window.get(c) || 0) + 1); + if (window.get(c) === need.get(c)) { + valid++; + } + } + + // 判断左侧窗口是否要收缩 + while (right - left >= t.length) { + // 在这里判断是否找到了合法的子串 + if (valid === need.size) { + return true; + } + const d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + if (need.has(d)) { + if (window.get(d) === need.get(d)) { + valid--; + } + window.set(d, (window.get(d) || 0) - 1); + } + } + } + // 未找到符合条件的子串 + return false; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def checkInclusion(self, t: str, s: str) -> bool: + need, window = {}, {} + for c in t: + need[c] = need.get(c, 0) + 1 + + left, right = 0, 0 + valid = 0 + while right < len(s): + c = s[right] + right += 1 + # 进行窗口内数据的一系列更新 + if c in need: + window[c] = window.get(c, 0) + 1 + if window[c] == need[c]: + valid += 1 + + # 判断左侧窗口是否要收缩 + while right - left >= len(t): + # 在这里判断是否找到了合法的子串 + if valid == len(need): + return True + d = s[left] + left += 1 + # 进行窗口内数据的一系列更新 + if d in need: + if window[d] == need[d]: + valid -= 1 + window[d] -= 1 + + # 未找到符合条件的子串 + return False +``` + +https://leetcode.cn/problems/MPnaiL 的多语言解法👆 + +https://leetcode.cn/problems/N6YdxV 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int searchInsert(vector& nums, int target) { + return left_bound(nums, target); + } + + // 搜索左侧边界的二分算法 + int left_bound(vector& nums, int target) { + if (nums.size() == 0) return -1; + int left = 0; + int right = nums.size(); // 注意 + + while (left < right) { // 注意 + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; // 注意 + } + } + return left; + } +}; +``` + +```go +// by chatGPT (go) +func searchInsert(nums []int, target int) int { + return leftBound(nums, target) +} + +//搜索左侧边界的二分算法 +func leftBound(nums []int, target int) int { + if len(nums) == 0 { + return -1 + } + left, right := 0, len(nums) + + for left < right { + mid := left + (right - left)/2 + if nums[mid] == target { + right = mid + } else if nums[mid] < target { + left = mid + 1 + } else if nums[mid] > target { + right = mid + } + } + return left +} +``` + +```java +// by labuladong (java) +class Solution { + public int searchInsert(int[] nums, int target) { + return left_bound(nums, target); + } + + // 搜索左侧边界的二分算法 + int left_bound(int[] nums, int target) { + if (nums.length == 0) return -1; + int left = 0; + int right = nums.length; // 注意 + + while (left < right) { // 注意 + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; // 注意 + } + } + return left; + } +} +``` + +```javascript +// by chatGPT (javascript) +var Solution = function() {}; + +Solution.prototype.searchInsert = function(nums, target) { + return this.left_bound(nums, target); +}; + +// 搜索左侧边界的二分算法 +Solution.prototype.left_bound = function(nums, target) { + if (nums.length == 0) return -1; + var left = 0; + var right = nums.length; // 注意 + + while (left < right) { // 注意 + var mid = left + Math.floor((right - left) / 2); + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; // 注意 + } + } + return left; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + return self.left_bound(nums, target) + + # 搜索左侧边界的二分算法 + def left_bound(self, nums: List[int], target: int) -> int: + if len(nums) == 0: + return -1 + left, right = 0, len(nums) # 注意 + + while left < right: # 注意 + mid = left + (right - left) // 2 + if nums[mid] == target: + right = mid + elif nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid # 注意 + + return left +``` + +https://leetcode.cn/problems/N6YdxV 的多语言解法👆 + +https://leetcode.cn/problems/NUPfPr 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool canPartition(vector& nums) { + int sum = 0; + for (int num : nums) sum += num; + // 和为奇数时,不可能划分成两个和相等的集合 + if (sum % 2 != 0) return false; + int n = nums.size(); + sum = sum / 2; + vector> dp(n + 1, vector(sum + 1, false)); + // base case + for (int i = 0; i <= n; i++) + dp[i][0] = true; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= sum; j++) { + if (j - nums[i - 1] < 0) { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j]; + } else { + // 装入或不装入背包 + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; + } + } + } + return dp[n][sum]; + } +}; +``` + +```go +// by chatGPT (go) +func canPartition(nums []int) bool { + sum := 0 + for _, num := range nums { + sum += num + } + // 和为奇数时,不可能划分成两个和相等的集合 + if sum%2 != 0 { + return false + } + n := len(nums) + sum = sum / 2 + dp := make([][]bool, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]bool, sum+1) + // base case + dp[i][0] = true + } + + for i := 1; i <= n; i++ { + for j := 1; j <= sum; j++ { + if j-nums[i-1] < 0 { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i-1][j] + } else { + // 装入或不装入背包 + dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]] + } + } + } + return dp[n][sum] +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean canPartition(int[] nums) { + int sum = 0; + for (int num : nums) sum += num; + // 和为奇数时,不可能划分成两个和相等的集合 + if (sum % 2 != 0) return false; + int n = nums.length; + sum = sum / 2; + boolean[][] dp = new boolean[n + 1][sum + 1]; + // base case + for (int i = 0; i <= n; i++) + dp[i][0] = true; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= sum; j++) { + if (j - nums[i - 1] < 0) { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j]; + } else { + // 装入或不装入背包 + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; + } + } + } + return dp[n][sum]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var canPartition = function(nums) { + let sum = 0; + for (let num of nums) { + sum += num; + } + // 和为奇数时,不可能划分成两个和相等的集合 + if (sum % 2 !== 0) { + return false; + } + let n = nums.length; + sum = sum / 2; + let dp = new Array(n + 1).fill(false).map(() => new Array(sum + 1).fill(false)); + // base case + for (let i = 0; i <= n; i++) { + dp[i][0] = true; + } + + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= sum; j++) { + if (j - nums[i - 1] < 0) { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j]; + } else { + // 装入或不装入背包 + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; + } + } + } + return dp[n][sum]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def canPartition(self, nums: List[int]) -> bool: + sum = 0 + for num in nums: + sum += num + # 和为奇数时,不可能划分成两个和相等的集合 + if sum % 2 != 0: + return False + n = len(nums) + sum = sum // 2 + dp = [[False] * (sum + 1) for _ in range(n + 1)] + # base case + for i in range(n + 1): + dp[i][0] = True + + for i in range(1, n + 1): + for j in range(1, sum + 1): + if j - nums[i - 1] < 0: + # 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j] + else: + # 装入或不装入背包 + dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]] + return dp[n][sum] +``` + +https://leetcode.cn/problems/NUPfPr 的多语言解法👆 + +https://leetcode.cn/problems/O4NDxx 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class NumMatrix { +private: + // preSum[i][j] 记录矩阵 [0, 0, i, j] 的元素和 + vector> preSum; + +public: + NumMatrix(vector>& matrix) { + int m = matrix.size(), n = matrix[0].size(); + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = vector>(m + 1, vector(n + 1, 0)); + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1]; + } + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + int sumRegion(int x1, int y1, int x2, int y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]; + } +}; +``` + +```go +// by chatGPT (go) +type NumMatrix struct { + // preSum[i][j] 记录矩阵 [0, 0, i, j] 的元素和 + preSum [][]int +} + +func Constructor(matrix [][]int) NumMatrix { + m, n := len(matrix), len(matrix[0]) + if m == 0 || n == 0 { + return NumMatrix{} + } + // 构造前缀和矩阵 + preSum := make([][]int, m+1) + for i := range preSum { + preSum[i] = make([]int, n+1) + } + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i-1][j-1] - preSum[i-1][j-1] + } + } + return NumMatrix{preSum} +} + +// 计算子矩阵 [x1, y1, x2, y2] 的元素和 +func (this *NumMatrix) SumRegion(x1 int, y1 int, x2 int, y2 int) int { + // 目标矩阵之和由四个相邻矩阵运算获得 + return this.preSum[x2+1][y2+1] - this.preSum[x1][y2+1] - this.preSum[x2+1][y1] + this.preSum[x1][y1] +} +``` + +```java +// by labuladong (java) +class NumMatrix { + // preSum[i][j] 记录矩阵 [0, 0, i-1, j-1] 的元素和 + private int[][] preSum; + + public NumMatrix(int[][] matrix) { + int m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1]; + } + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + public int sumRegion(int x1, int y1, int x2, int y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var NumMatrix = function(matrix) { + // preSum[i][j] 记录矩阵 [0, 0, i, j] 的元素和 + var preSum; + + var m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = new Array(m + 1); + for (var i = 0; i <= m; i++) { + preSum[i] = new Array(n + 1).fill(0); + } + for (var i = 1; i <= m; i++) { + for (var j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1]; + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + this.sumRegion = function(x1, y1, x2, y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]; + } +}; +``` + +```python +# by chatGPT (python) +class NumMatrix: + def __init__(self, matrix: List[List[int]]): + m, n = len(matrix), len(matrix[0]) + if m == 0 or n == 0: + return + # 构造前缀和矩阵 + self.preSum = [[0] * (n + 1) for _ in range(m + 1)] + for i in range(1, m + 1): + for j in range(1, n + 1): + # 计算每个矩阵 [0, 0, i, j] 的元素和 + self.preSum[i][j] = self.preSum[i - 1][j] + self.preSum[i][j - 1] + matrix[i - 1][j - 1] - self.preSum[i - 1][j - 1] + + # 计算子矩阵 [x1, y1, x2, y2] 的元素和 + def sumRegion(self, x1: int, y1: int, x2: int, y2: int) -> int: + # 目标矩阵之和由四个相邻矩阵运算获得 + return self.preSum[x2 + 1][y2 + 1] - self.preSum[x1][y2 + 1] - self.preSum[x2 + 1][y1] + self.preSum[x1][y1] +``` + +https://leetcode.cn/problems/O4NDxx 的多语言解法👆 + +https://leetcode.cn/problems/OrIXps 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class LRUCache { + int cap; + unordered_map cache; + list keys; + +public: + LRUCache(int capacity) { + this->cap = capacity; + } + + int get(int key) { + auto it = cache.find(key); + if (it == cache.end()) { + return -1; + } + // 将 key 变为最近使用 + makeRecently(key); + return it->second; + } + + void put(int key, int val) { + auto it = cache.find(key); + if (it != cache.end()) { + // 修改 key 的值 + it->second = val; + // 将 key 变为最近使用 + makeRecently(key); + return; + } + + if (cache.size() >= this->cap) { + // 链表头部就是最久未使用的 key + int oldestKey = keys.front(); + keys.pop_front(); + cache.erase(oldestKey); + } + // 将新的 key 添加链表尾部 + keys.push_back(key); + cache[key] = val; + } + +private: + void makeRecently(int key) { + int val = cache[key]; + // 删除 key,重新插入到队尾 + keys.remove(key); + keys.push_back(key); + cache[key] = val; + } +}; +``` + +```go +// by chatGPT (go) +type LRUCache struct { + cap int + cache map[int]int +} + +// Constructor 创建一个 LRU Cache 实例 +func Constructor(capacity int) LRUCache { + return LRUCache{ + cap: capacity, + cache: make(map[int]int), + } +} + +// Get 获取一个 key 的值 +func (this *LRUCache) Get(key int) int { + if val, ok := this.cache[key]; ok { + this.makeRecently(key) + return val + } + return -1 +} + +// Put 插入一个 key/value +func (this *LRUCache) Put(key int, value int) { + if _, ok := this.cache[key]; ok { + this.cache[key] = value + this.makeRecently(key) + return + } + if len(this.cache) >= this.cap { + this.removeLeastRecently() + } + this.cache[key] = value +} + +// makeRecently 将一个元素标记为最近使用的 +func (this *LRUCache) makeRecently(key int) { + val := this.cache[key] + delete(this.cache, key) + this.cache[key] = val +} + +// removeLeastRecently 移除最近未使用的元素 +func (this *LRUCache) removeLeastRecently() { + for k := range this.cache { + delete(this.cache, k) + break + } +} +``` + +```java +// by labuladong (java) +class LRUCache { + int cap; + LinkedHashMap cache = new LinkedHashMap<>(); + public LRUCache(int capacity) { + this.cap = capacity; + } + + public int get(int key) { + if (!cache.containsKey(key)) { + return -1; + } + // 将 key 变为最近使用 + makeRecently(key); + return cache.get(key); + } + + public void put(int key, int val) { + if (cache.containsKey(key)) { + // 修改 key 的值 + cache.put(key, val); + // 将 key 变为最近使用 + makeRecently(key); + return; + } + + if (cache.size() >= this.cap) { + // 链表头部就是最久未使用的 key + int oldestKey = cache.keySet().iterator().next(); + cache.remove(oldestKey); + } + // 将新的 key 添加链表尾部 + cache.put(key, val); + } + + private void makeRecently(int key) { + int val = cache.get(key); + // 删除 key,重新插入到队尾 + cache.remove(key); + cache.put(key, val); + } +} +``` + +```javascript +// by chatGPT (javascript) +var LRUCache = function(capacity) { + this.cap = capacity; + this.cache = new Map(); +}; + +LRUCache.prototype.get = function(key) { + if (!this.cache.has(key)) { + return -1; + } + // 将 key 变为最近使用 + this.makeRecently(key); + return this.cache.get(key); +}; + +LRUCache.prototype.put = function(key, val) { + if (this.cache.has(key)) { + // 修改 key 的值 + this.cache.set(key, val); + // 将 key 变为最近使用 + this.makeRecently(key); + return; + } + + if (this.cache.size >= this.cap) { + // 链表头部就是最久未使用的 key + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + // 将新的 key 添加链表尾部 + this.cache.set(key, val); +}; + +LRUCache.prototype.makeRecently = function(key) { + const val = this.cache.get(key); + // 删除 key,重新插入到队尾 + this.cache.delete(key); + this.cache.set(key, val); +}; +``` + +```python +# by chatGPT (python) +class LRUCache: + def __init__(self, capacity: int): + self.cap = capacity + self.cache = {} + + def get(self, key: int) -> int: + if key not in self.cache: + return -1 + # 将 key 变为最近使用 + self.makeRecently(key) + return self.cache[key] + + def put(self, key: int, val: int) -> None: + if key in self.cache: + # 修改 key 的值 + self.cache[key] = val + # 将 key 变为最近使用 + self.makeRecently(key) + return + + if len(self.cache) >= self.cap: + # 链表头部就是最久未使用的 key + oldest_key = next(iter(self.cache)) + self.cache.pop(oldest_key) + + # 将新的 key 添加链表尾部 + self.cache[key] = val + + def makeRecently(self, key: int) -> None: + val = self.cache[key] + # 删除 key,重新插入到队尾 + del self.cache[key] + self.cache[key] = val +``` + +https://leetcode.cn/problems/OrIXps 的多语言解法👆 + +https://leetcode.cn/problems/PzWKhm 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int rob(vector& nums) { + int n = nums.size(); + if (n == 1) return nums[0]; + + vector memo1(n, -1); + vector memo2(n, -1); + // 两次调用使用两个不同的备忘录 + return max( + dp(nums, 0, n - 2, memo1), + dp(nums, 1, n - 1, memo2) + ); + } + + // 定义:计算闭区间 [start,end] 的最优结果 + int dp(vector& nums, int start, int end, vector& memo) { + if (start > end) { + return 0; + } + + if (memo[start] != -1) { + return memo[start]; + } + // 状态转移方程 + int res = max( + dp(nums, start + 2, end, memo) + nums[start], + dp(nums, start + 1, end, memo) + ); + + memo[start] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +import "fmt" + +func rob(nums []int) int { + n := len(nums) + if n == 1 { + return nums[0] + } + + memo1 := make([]int, n) + memo2 := make([]int, n) + for i := range memo1 { + memo1[i] = -1 + memo2[i] = -1 + } + // 两次调用使用两个不同的备忘录 + return max(dp(nums, 0, n - 2, memo1), dp(nums, 1, n - 1, memo2)) +} + +// 定义:计算闭区间 [start,end] 的最优结果 +func dp(nums []int, start, end int, memo []int) int { + if start > end { + return 0 + } + + if memo[start] != -1 { + return memo[start] + } + // 状态转移方程 + res := max(dp(nums, start + 2, end, memo) + nums[start], dp(nums, start + 1, end, memo)) + + memo[start] = res + return res +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + + public int rob(int[] nums) { + int n = nums.length; + if (n == 1) return nums[0]; + + int[] memo1 = new int[n]; + int[] memo2 = new int[n]; + Arrays.fill(memo1, -1); + Arrays.fill(memo2, -1); + // 两次调用使用两个不同的备忘录 + return Math.max( + dp(nums, 0, n - 2, memo1), + dp(nums, 1, n - 1, memo2) + ); + } + + // 定义:计算闭区间 [start,end] 的最优结果 + int dp(int[] nums, int start, int end, int[] memo) { + if (start > end) { + return 0; + } + + if (memo[start] != -1) { + return memo[start]; + } + // 状态转移方程 + int res = Math.max( + dp(nums, start + 2, end, memo) + nums[start], + dp(nums, start + 1, end, memo) + ); + + memo[start] = res; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var rob = function(nums) { + const n = nums.length; + if (n === 1) return nums[0]; + + const memo1 = new Array(n).fill(-1); + const memo2 = new Array(n).fill(-1); + // 两次调用使用两个不同的备忘录 + return Math.max( + dp(nums, 0, n - 2, memo1), + dp(nums, 1, n - 1, memo2) + ); +}; + +// 定义:计算闭区间 [start,end] 的最优结果 +function dp(nums, start, end, memo) { + if (start > end) { + return 0; + } + + if (memo[start] !== -1) { + return memo[start]; + } + // 状态转移方程 + const res = Math.max( + dp(nums, start + 2, end, memo) + nums[start], + dp(nums, start + 1, end, memo) + ); + + memo[start] = res; + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def rob(self, nums: List[int]) -> int: + n = len(nums) + if n == 1: + return nums[0] + memo1 = [-1]*n + memo2 = [-1]*n + # 两次调用使用两个不同的备忘录 + return max( + self.dp(nums, 0, n-2, memo1), + self.dp(nums, 1, n-1, memo2) + ) + + # 定义:计算闭区间 [start,end] 的最优结果 + def dp(self, nums: List[int], start: int, end: int, memo: List[int]) -> int: + if start > end: + return 0 + if memo[start] != -1: + return memo[start] + # 状态转移方程 + res = max( + self.dp(nums, start+2, end, memo) + nums[start], + self.dp(nums, start+1, end, memo) + ) + memo[start] = res + return res +``` + +https://leetcode.cn/problems/PzWKhm 的多语言解法👆 + +https://leetcode.cn/problems/QA2IGt 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector findOrder(int numCourses, vector>& prerequisites) { + // 建图,和环检测算法相同 + vector> graph(numCourses); + for (auto& edge : prerequisites) { + int from = edge[1], to = edge[0]; + graph[from].push_back(to); + } + // 计算入度,和环检测算法相同 + vector indegree(numCourses); + for (auto& edge : prerequisites) { + int from = edge[1], to = edge[0]; + indegree[to]++; + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + queue q; + for (int i = 0; i < numCourses; i++) { + if (indegree[i] == 0) { + q.push(i); + } + } + + // 记录拓扑排序结果 + vector res(numCourses); + // 记录遍历节点的顺序(索引) + int count = 0; + // 开始执行 BFS 算法 + while (!q.empty()) { + int cur = q.front(); + q.pop(); + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur; + count++; + for (int next : graph[cur]) { + indegree[next]--; + if (indegree[next] == 0) { + q.push(next); + } + } + } + + if (count != numCourses) { + // 存在环,拓扑排序不存在 + return {}; + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +// 主函数 +func findOrder(numCourses int, prerequisites [][]int) []int { + // 建图,和环检测算法相同 + graph := buildGraph(numCourses, prerequisites) + // 计算入度,和环检测算法相同 + indegree := make([]int, numCourses) + for _, edge := range prerequisites { + _, to := edge[1], edge[0] + indegree[to]++ + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + q := make([]int, 0) + for i := 0; i < numCourses; i++ { + if indegree[i] == 0 { + q = append(q, i) + } + } + + // 记录拓扑排序结果 + res := make([]int, numCourses) + // 记录遍历节点的顺序(索引) + count := 0 + // 开始执行 BFS 算法 + for len(q) > 0 { + cur := q[0] + q = q[1:] + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur + count++ + for _, next := range graph[cur] { + indegree[next]-- + if indegree[next] == 0 { + q = append(q, next) + } + } + } + + if count != numCourses { + // 存在环,拓扑排序不存在 + return []int{} + } + + return res +} + +// 建图函数 +func buildGraph(numCourses int, prerequisites [][]int) [] []int { + // 图中共有 numCourses 个节点 + graph := make([][]int, numCourses) + for i := 0; i < numCourses; i++ { + graph[i] = make([]int, 0) + } + for _, edge := range prerequisites { + from, to := edge[1], edge[0] + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from] = append(graph[from], to) + } + return graph +} +``` + +```java +// by labuladong (java) +class Solution { + // 主函数 + public int[] findOrder(int numCourses, int[][] prerequisites) { + // 建图,和环检测算法相同 + List[] graph = buildGraph(numCourses, prerequisites); + // 计算入度,和环检测算法相同 + int[] indegree = new int[numCourses]; + for (int[] edge : prerequisites) { + int from = edge[1], to = edge[0]; + indegree[to]++; + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + Queue q = new LinkedList<>(); + for (int i = 0; i < numCourses; i++) { + if (indegree[i] == 0) { + q.offer(i); + /** + ![](../pictures/拓扑排序/6.jpeg) + */ + } + } + + // 记录拓扑排序结果 + int[] res = new int[numCourses]; + // 记录遍历节点的顺序(索引) + int count = 0; + // 开始执行 BFS 算法 + while (!q.isEmpty()) { + int cur = q.poll(); + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur; + count++; + for (int next : graph[cur]) { + /** + ![](../pictures/拓扑排序/7.jpeg) + */ + indegree[next]--; + if (indegree[next] == 0) { + q.offer(next); + } + } + } + + if (count != numCourses) { + // 存在环,拓扑排序不存在 + return new int[]{}; + } + + return res; + } + + // 建图函数 + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 图中共有 numCourses 个节点 + List[] graph = new LinkedList[numCourses]; + for (int i = 0; i < numCourses; i++) { + graph[i] = new LinkedList<>(); + } + for (int[] edge : prerequisites) { + int from = edge[1], to = edge[0]; + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].add(to); + } + return graph; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findOrder = function(numCourses, prerequisites) { + // 建图,和环检测算法相同 + const graph = buildGraph(numCourses, prerequisites); + // 计算入度,和环检测算法相同 + const indegree = new Array(numCourses).fill(0); + for (const [from, to] of prerequisites) { + indegree[to]++; + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + const q = []; + for (let i = 0; i < numCourses; i++) { + if (indegree[i] === 0) { + q.push(i); + /** + ![](../pictures/拓扑排序/6.jpeg) + */ + } + } + + // 记录拓扑排序结果 + const res = new Array(numCourses); + // 记录遍历节点的顺序(索引) + let count = 0; + // 开始执行 BFS 算法 + while (q.length > 0) { + const cur = q.shift(); + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur; + count++; + for (const next of graph[cur]) { + /** + ![](../pictures/拓扑排序/7.jpeg) + */ + indegree[next]--; + if (indegree[next] === 0) { + q.push(next); + } + } + } + + if (count !== numCourses) { + // 存在环,拓扑排序不存在 + return []; + } + + return res; +}; + +// 建图函数 +function buildGraph(numCourses, prerequisites) { + // 图中共有 numCourses 个节点 + const graph = new Array(numCourses).map(() => []); + for (const [from, to] of prerequisites) { + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].push(to); + } + return graph; +} +``` + +```python +# by chatGPT (python) +class Solution: + def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]: + # 建图,和环检测算法相同 + graph = self.buildGraph(numCourses, prerequisites) + # 计算入度,和环检测算法相同 + indegree = [0] * numCourses + for edge in prerequisites: + from_, to = edge[1], edge[0] + indegree[to] += 1 + + # 根据入度初始化队列中的节点,和环检测算法相同 + q = collections.deque() + for i in range(numCourses): + if indegree[i] == 0: + q.append(i) + + # 记录拓扑排序结果 + res = [0] * numCourses + # 记录遍历节点的顺序(索引) + count = 0 + # 开始执行 BFS 算法 + while q: + cur = q.popleft() + # 弹出节点的顺序即为拓扑排序结果 + res[count] = cur + count += 1 + for next_ in graph[cur]: + indegree[next_] -= 1 + if indegree[next_] == 0: + q.append(next_) + + if count != numCourses: + # 存在环,拓扑排序不存在 + return [] + + return res + + # 建图函数 + def buildGraph(self, numCourses: int, prerequisites: List[List[int]]) -> List[List[int]]: + # 图中共有 numCourses 个节点 + graph = [[] for _ in range(numCourses)] + for edge in prerequisites: + from_, to = edge[1], edge[0] + # 修完课程 from 才能修课程 to + # 在图中添加一条从 from 指向 to 的有向边 + graph[from_].append(to) + return graph +``` + +https://leetcode.cn/problems/QA2IGt 的多语言解法👆 + +https://leetcode.cn/problems/SLwz0R 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 主函数 + ListNode* removeNthFromEnd(ListNode* head, int n) { + // 虚拟头结点 + ListNode* dummy = new ListNode(-1); + dummy->next = head; + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + ListNode* x = findFromEnd(dummy, n + 1); + // 删掉倒数第 n 个节点 + x->next = x->next->next; + return dummy->next; + } + + // 返回链表的倒数第 k 个节点 + ListNode* findFromEnd(ListNode* head, int k) { + ListNode* p1 = head; + // p1 先走 k 步 + for (int i = 0; i < k; i++) { + p1 = p1->next; + } + ListNode* p2 = head; + // p1 和 p2 同时走 n - k 步 + while (p1 != nullptr) { + p2 = p2->next; + p1 = p1->next; + } + // p2 现在指向第 n - k 个节点 + return p2; + } +}; +``` + +```go +// by chatGPT (go) +// removeNthFromEnd 主函数 +func removeNthFromEnd(head *ListNode, n int) *ListNode { + // 虚拟头结点 + dummy := &ListNode{-1, head} + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + x := findFromEnd(dummy, n + 1) + // 删掉倒数第 n 个节点 + x.Next = x.Next.Next + return dummy.Next +} + +// findFromEnd 返回链表的倒数第 k 个节点 +func findFromEnd(head *ListNode, k int) *ListNode { + p1 := head + // p1 先走 k 步 + for i := 0; i < k; i++ { + p1 = p1.Next + } + p2 := head + // p1 和 p2 同时走 n - k 步 + for p1 != nil { + p2 = p2.Next + p1 = p1.Next + } + // p2 现在指向第 n - k 个节点 + return p2 +} +``` + +```java +// by labuladong (java) +class Solution { + // 主函数 + public ListNode removeNthFromEnd(ListNode head, int n) { + // 虚拟头结点 + ListNode dummy = new ListNode(-1); + dummy.next = head; + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + ListNode x = findFromEnd(dummy, n + 1); + // 删掉倒数第 n 个节点 + x.next = x.next.next; + return dummy.next; + } + + // 返回链表的倒数第 k 个节点 + ListNode findFromEnd(ListNode head, int k) { + ListNode p1 = head; + // p1 先走 k 步 + for (int i = 0; i < k; i++) { + p1 = p1.next; + } + ListNode p2 = head; + // p1 和 p2 同时走 n - k 步 + while (p1 != null) { + p2 = p2.next; + p1 = p1.next; + } + // p2 现在指向第 n - k 个节点 + return p2; + } +} +``` + +```javascript +// by chatGPT (javascript) +var removeNthFromEnd = function(head, n) { + // 虚拟头结点 + var dummy = new ListNode(-1); + dummy.next = head; + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + var x = findFromEnd(dummy, n + 1); + // 删掉倒数第 n 个节点 + x.next = x.next.next; + return dummy.next; +}; + +// 返回链表的倒数第 k 个节点 +var findFromEnd = function(head, k) { + var p1 = head; + // p1 先走 k 步 + for (var i = 0; i < k; i++) { + p1 = p1.next; + } + var p2 = head; + // p1 和 p2 同时走 n - k 步 + while (p1 != null) { + p2 = p2.next; + p1 = p1.next; + } + // p2 现在指向第 n - k 个节点 + return p2; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: + # 虚拟头结点 + dummy = ListNode(-1) + dummy.next = head + # 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + x = self.findFromEnd(dummy, n + 1) + # 删掉倒数第 n 个节点 + x.next = x.next.next + return dummy.next + + # 返回链表的倒数第 k 个节点 + def findFromEnd(self, head: ListNode, k: int) -> ListNode: + p1 = head + # p1 先走 k 步 + for i in range(k): + p1 = p1.next + p2 = head + # p1 和 p2 同时走 n - k 步 + while p1: + p2 = p2.next + p1 = p1.next + # p2 现在指向第 n - k 个节点 + return p2 +``` + +https://leetcode.cn/problems/SLwz0R 的多语言解法👆 + +https://leetcode.cn/problems/SsGoHC 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> merge(vector>& intervals) { + vector> res; + // 按区间的 start 升序排列 + sort(intervals.begin(), intervals.end(), [](vector& a, vector& b) { + return a[0] < b[0]; + }); + + res.push_back(intervals[0]); + for (int i = 1; i < intervals.size(); i++) { + vector& curr = intervals[i]; + // res 中最后一个元素的引用 + vector& last = res.back(); + if (curr[0] <= last[1]) { + last[1] = max(last[1], curr[1]); + } else { + // 处理下一个待合并区间 + res.push_back(curr); + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func merge(intervals [][]int) [][]int { + res := [][]int{} + // 按区间的 start 升序排列 + sort.Slice(intervals, func(i, j int) bool { + return intervals[i][0] < intervals[j][0] + }) + + res = append(res, intervals[0]) + for i := 1; i < len(intervals); i++ { + curr := intervals[i] + // res 中最后一个元素的引用 + last := res[len(res)-1] + if curr[0] <= last[1] { + last[1] = max(last[1], curr[1]) + } else { + // 处理下一个待合并区间 + res = append(res, curr) + } + } + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] merge(int[][] intervals) { + LinkedList res = new LinkedList<>(); + // 按区间的 start 升序排列 + Arrays.sort(intervals, (a, b) -> { + return a[0] - b[0]; + }); + + res.add(intervals[0]); + for (int i = 1; i < intervals.length; i++) { + int[] curr = intervals[i]; + // res 中最后一个元素的引用 + int[] last = res.getLast(); + if (curr[0] <= last[1]) { + last[1] = Math.max(last[1], curr[1]); + } else { + // 处理下一个待合并区间 + res.add(curr); + } + } + return res.toArray(new int[0][0]); + } +} +``` + +```javascript +// by chatGPT (javascript) +var merge = function(intervals) { + let res = []; + // 按区间的 start 升序排列 + intervals.sort((a, b) => { + return a[0] - b[0]; + }); + + res.push(intervals[0]); + for (let i = 1; i < intervals.length; i++) { + let curr = intervals[i]; + // res 中最后一个元素的引用 + let last = res[res.length - 1]; + if (curr[0] <= last[1]) { + last[1] = Math.max(last[1], curr[1]); + } else { + // 处理下一个待合并区间 + res.push(curr); + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def merge(self, intervals: List[List[int]]) -> List[List[int]]: + res = [] + # 按区间的 start 升序排列 + intervals.sort(key=lambda x: x[0]) + res.append(intervals[0]) + for curr in intervals[1:]: + # res 中最后一个元素的引用 + last = res[-1] + if curr[0] <= last[1]: + last[1] = max(last[1], curr[1]) + else: + # 处理下一个待合并区间 + res.append(curr) + return res +``` + +https://leetcode.cn/problems/SsGoHC 的多语言解法👆 + +https://leetcode.cn/problems/TVdhkn 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector> res; + vector> subsets(vector& nums) { + // 记录走过的路径 + vector track; + backtrack(nums, 0, track); + return res; + } + + void backtrack(vector& nums, int start, vector& track) { + res.push_back(track); + for (int i = start; i < nums.size(); i++) { + // 做选择 + track.push_back(nums[i]); + // 回溯 + backtrack(nums, i + 1, track); + // 撤销选择 + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +// LeetCode Solution for Subsets +func subsets(nums []int) [][]int { + var res [][]int + backtrack := func(nums []int, start int, track []int) { + tmp := make([]int, len(track)) + copy(tmp, track) + res = append(res, tmp) + for i := start; i < len(nums); i++ { + // 做选择 + track = append(track, nums[i]) + // 回溯 + backtrack(nums, i+1, track) + // 撤销选择 + track = track[:len(track)-1] + } + } + backtrack(nums, 0, []int{}) + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + List> res = new ArrayList<>(); + + public List> subsets(int[] nums) { + List track = new ArrayList<>(); + backtrack(nums, 0, track); + return res; + } + + private void backtrack(int[] nums, int start, List track) { + res.add(new ArrayList<>(track)); + for (int i = start; i < nums.length; i++) { + // 做选择 + track.add(nums[i]); + // 回溯 + backtrack(nums, i + 1, track); + // 撤销选择 + track.remove(track.size() - 1); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var subsets = function(nums) { + // 记录走过的路径 + var res = []; + var backtrack = function(nums, start, track) { + res.push(track.slice()); // 添加路径的拷贝 + for (var i = start; i < nums.length; i++) { + // 做选择 + track.push(nums[i]); + // 回溯 + backtrack(nums, i + 1, track); + // 撤销选择 + track.pop(); + } + } + var track = []; + backtrack(nums, 0, track); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + + def subsets(self, nums: List[int]) -> List[List[int]]: + # 记录走过的路径 + track = [] + self.backtrack(nums, 0, track) + return self.res + + def backtrack(self, nums: List[int], start: int, track: List[int]) -> None: + self.res.append(track[:]) + for i in range(start, len(nums)): + # 做选择 + track.append(nums[i]) + # 回溯 + self.backtrack(nums, i + 1, track) + # 撤销选择 + track.pop() +``` + +https://leetcode.cn/problems/TVdhkn 的多语言解法👆 + +https://leetcode.cn/problems/UHnkqh 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* reverseList(ListNode* head) { + if (head == nullptr || head->next == nullptr) { + return head; + } + ListNode* last = reverseList(head->next); + /** + ![](../pictures/反转链表/3.jpg) + */ + head->next->next = head; + /** + ![](../pictures/反转链表/4.jpg) + */ + head->next = nullptr; + /** + ![](../pictures/反转链表/5.jpg) + */ + return last; + } +}; +``` + +```go +// by chatGPT (go) +func removeNthFromEnd(head *ListNode, n int) *ListNode { + // 哑节点,方便处理删除头节点的情况 + dummy := &ListNode{Val: 0, Next: head} + + // 快指针先走 n 步 + fast := head + for i := 0; i < n; i++ { + fast = fast.Next + } + + // 慢指针和快指针一起走,直到快指针到达链表尾部 + slow := dummy + for fast != nil { + slow = slow.Next + fast = fast.Next + } + + // 删除倒数第 n 个节点 + slow.Next = slow.Next.Next + + // 返回头节点 + return dummy.Next +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode last = reverseList(head.next); + /** + ![](../pictures/反转链表/3.jpg) + */ + head.next.next = head; + /** + ![](../pictures/反转链表/4.jpg) + */ + head.next = null; + /** + ![](../pictures/反转链表/5.jpg) + */ + return last; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 + * + * 示例 1: + * + * 输入: "abcabcbb" + * 输出: 3 + * 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 + * + * 示例 2: + * + * 输入: "bbbbb" + * 输出: 1 + * 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 + * + * 示例 3: + * + * 输入: "pwwkew" + * 输出: 3 + * 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 + * 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 + */ + +var lengthOfLongestSubstring = function(s) { + let n = s.length, ans = 0; + let index = new Map(); // 存储字符上一次出现的位置 + // i为左指针,j为右指针 + for (let j = 0, i = 0; j < n; j++) { + if (index.has(s[j])) { + // 如果有重复的字符,移动左指针到上一个同样字符的后一个位置 + i = Math.max(index.get(s[j]) + 1, i); + } + ans = Math.max(ans, j - i + 1); // 更新结果 + index.set(s[j], j); // 更新字符的位置 + } + return ans; +}; +``` + +```python +# by chatGPT (python) +# 请保留所有中文注释! +class Solution: + def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: + # 新建一个虚拟节点,避免特判头节点的情况 + dummy = ListNode(0, head) + # 快指针先走n步 + fast = head + for i in range(n): + fast = fast.next + # 慢指针开始走,与快指针保持n步的距离 + slow = dummy + while fast is not None: + fast = fast.next + slow = slow.next + # 此时slow指向的是倒数第n+1个节点,删除下一个节点即可 + slow.next = slow.next.next + return dummy.next +``` + +https://leetcode.cn/problems/UHnkqh 的多语言解法👆 + +https://leetcode.cn/problems/VabMRr 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector findAnagrams(string s, string t) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + vector res; // 记录结果 + while (right < s.size()) { + char c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + if (need.count(c)) { + window[c]++; + if (window[c] == need[c]) + valid++; + } + // 判断左侧窗口是否要收缩 + while (right - left >= t.size()) { + // 当窗口符合条件时,把起始索引加入 res + if (valid == need.size()) + res.push_back(left); + char d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + if (need.count(d)) { + if (window[d] == need[d]) + valid--; + window[d]--; + } + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func findAnagrams(s string, t string) []int { + need, window := make(map[byte]int), make(map[byte]int) + for i := range t { + need[t[i]]++ + } + + left, right := 0, 0 + valid := 0 + var res []int + for right < len(s) { + c := s[right] + right++ + // 进行窗口内数据的一系列更新 + update := func(c byte) { + if _, ok := need[c]; ok { + window[c]++ + if window[c] == need[c] { + valid++ + } + } + } + update(c) + // 判断左侧窗口是否要收缩 + for right - left >= len(t) { + // 当窗口符合条件时,把起始索引加入 res + if valid == len(need) { + res = append(res, left) + } + d := s[left] + left++ + // 进行窗口内数据的一系列更新 + update := func(d byte) { + if _, ok := need[d]; ok { + if window[d] == need[d] { + valid-- + } + window[d]-- + } + } + update(d) + } + } + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + public List findAnagrams(String s, String t) { + Map need = new HashMap<>(); + Map window = new HashMap<>(); + for (char c : t.toCharArray()) { + need.put(c, need.getOrDefault(c, 0) + 1); + } + + int left = 0, right = 0, valid = 0; + List res = new ArrayList<>(); // 记录结果 + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) { + valid++; + } + } + // 判断左侧窗口是否要收缩 + while (right - left >= t.length()) { + // 当窗口符合条件时,把起始索引加入 res + if (valid == need.size()) { + res.add(left); + } + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) { + valid--; + } + window.put(d, window.get(d) - 1); + } + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findAnagrams = function(s, t) { + var need = {}, window = {}; + for (var i = 0; i < t.length; i++) { + var c = t[i]; + need[c] = (need[c] || 0) + 1; + } + + var left = 0, right = 0; + var valid = 0; + var res = []; + while (right < s.length) { + var c = s[right]; + right++; + + // 进行窗口内数据的一系列更新 + if (need.hasOwnProperty(c)) { + window[c] = (window[c] || 0) + 1; + if (window[c] === need[c]) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (right - left >= t.length) { + // 当窗口符合条件时,把起始索引加入 res + if (valid === Object.keys(need).length) + res.push(left); + var d = s[left]; + left++; + + // 进行窗口内数据的一系列更新 + if (need.hasOwnProperty(d)) { + if (window[d] === need[d]) + valid--; + window[d]--; + } + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findAnagrams(self, s: str, t: str) -> List[int]: + need, window = {}, {} + for c in t: + need[c] = need.get(c, 0) + 1 # 统计目标字符串中字符出现次数 + + left, right = 0, 0 + valid = 0 + res = [] + while right < len(s): + c = s[right] # 移入窗口的字符 + right += 1 + # 进行窗口内数据的更新 + if c in need: + window[c] = window.get(c, 0) + 1 + if window[c] == need[c]: + valid += 1 + + # 判断左侧窗口是否要收缩 + while right - left >= len(t): + # 当窗口符合条件时,把起始索引加入 res + if valid == len(need): + res.append(left) + d = s[left] # 移出窗口的字符 + left += 1 + # 进行窗口内数据的更新 + if d in need: + if window[d] == need[d]: + valid -= 1 + window[d] -= 1 + + return res +``` + +https://leetcode.cn/problems/VabMRr 的多语言解法👆 + +https://leetcode.cn/problems/VvJkup 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> res; + + /* 主函数,输入一组不重复的数字,返回它们的全排列 */ + vector> permute(vector& nums) { + // 记录「路径」 + vector track; + // 「路径」中的元素会被标记为 true,避免重复使用 + vector used(nums.size(), false); + + backtrack(nums, track, used); + return res; + } + + // 路径:记录在 track 中 + // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + // 结束条件:nums 中的元素全都在 track 中出现 + void backtrack(vector& nums, vector& track, vector& used) { + // 触发结束条件 + if (track.size() == nums.size()) { + res.push_back(track); + return; + } + + for (int i = 0; i < nums.size(); i++) { + // 排除不合法的选择 + if (used[i]) { + /** + ![](../pictures/backtracking/6.jpg) + */ + // nums[i] 已经在 track 中,跳过 + continue; + } + // 做选择 + track.push_back(nums[i]); + used[i] = true; + // 进入下一层决策树 + backtrack(nums, track, used); + // 取消选择 + track.pop_back(); + used[i] = false; + } + } +}; +``` + +```go +// by chatGPT (go) +func permute(nums []int) [][]int { + res := [][]int{} + + /* backtrack 函数会用到的参数 */ + var backtrack func(nums, track []int, used []bool) + backtrack = func(nums, track []int, used []bool) { + // 「取消选择」的过程是撤销上一次的选择,所以不需要结束条件 + if len(track) == len(nums) { + temp := make([]int, len(track)) + copy(temp, track) + res = append(res, temp) + return + } + + for i := 0; i < len(nums); i++ { + // 排除不合法的选择 + if used[i] { + continue + } + // 做选择 + track = append(track, nums[i]) + used[i] = true + // 进入下一层决策树 + backtrack(nums, track, used) + // 「取消选择」,进行回溯 + track = track[:len(track)-1] + used[i] = false + } + } + + backtrack(nums, []int{}, make([]bool, len(nums))) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + + List> res = new LinkedList<>(); + + /* 主函数,输入一组不重复的数字,返回它们的全排列 */ + List> permute(int[] nums) { + // 记录「路径」 + LinkedList track = new LinkedList<>(); + // 「路径」中的元素会被标记为 true,避免重复使用 + boolean[] used = new boolean[nums.length]; + + backtrack(nums, track, used); + return res; + } + + // 路径:记录在 track 中 + // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + // 结束条件:nums 中的元素全都在 track 中出现 + void backtrack(int[] nums, LinkedList track, boolean[] used) { + // 触发结束条件 + if (track.size() == nums.length) { + res.add(new LinkedList(track)); + return; + } + + for (int i = 0; i < nums.length; i++) { + // 排除不合法的选择 + if (used[i]) { + /** + ![](../pictures/backtracking/6.jpg) + */ + // nums[i] 已经在 track 中,跳过 + continue; + } + // 做选择 + track.add(nums[i]); + used[i] = true; + // 进入下一层决策树 + backtrack(nums, track, used); + // 取消选择 + track.removeLast(); + used[i] = false; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var permute = function(nums) { + const res = []; + + const backtrack = (track, used) => { + if (track.length === nums.length) { + res.push([...track]); + return; + } + + for (let i = 0; i < nums.length; i++) { + if (used[i]) { + // nums[i] 已经在 track 中,跳过 + continue; + } + track.push(nums[i]); + used[i] = true; + backtrack(track, used); + track.pop(); + used[i] = false; + } + }; + + backtrack([], []); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + + def permute(self, nums: List[int]) -> List[List[int]]: + track = [] + used = [False] * len(nums) + self.backtrack(nums, track, used) + return self.res + + def backtrack(self, nums: List[int], track: List[int], used: List[bool]) -> None: + if len(track) == len(nums): + self.res.append(track.copy()) + return + + for i in range(len(nums)): + if used[i]: + # nums[i] 已经在 track 中,跳过 + continue + track.append(nums[i]) + used[i] = True + self.backtrack(nums, track, used) + track.pop() + used[i] = False +``` + +https://leetcode.cn/problems/VvJkup 的多语言解法👆 + +https://leetcode.cn/problems/WhsWhI 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int longestConsecutive(vector& nums) { + // 转化成哈希集合,方便快速查找是否存在某个元素 + unordered_set set; + for (int num : nums) { + set.insert(num); + } + + int res = 0; + + for (int num : set) { + if (set.count(num - 1)) { + // num 不是连续子序列的第一个,跳过 + continue; + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + int curNum = num; + int curLen = 1; + + while (set.count(curNum + 1)) { + curNum += 1; + curLen += 1; + } + // 更新最长连续序列的长度 + res = max(res, curLen); + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func longestConsecutive(nums []int) int { + // 转化成哈希集合,方便快速查找是否存在某个元素 + set := make(map[int]bool) + for _, num := range nums { + set[num] = true + } + + res := 0 + + for num := range set { + if set[num-1] { + // num 不是连续子序列的第一个,跳过 + continue + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + curNum := num + curLen := 1 + + for set[curNum+1] { + curNum += 1 + curLen += 1 + } + // 更新最长连续序列的长度 + res = max(res, curLen) + } + + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int longestConsecutive(int[] nums) { + // 转化成哈希集合,方便快速查找是否存在某个元素 + HashSet set = new HashSet(); + for (int num : nums) { + set.add(num); + } + + int res = 0; + + for (int num : set) { + if (set.contains(num - 1)) { + // num 不是连续子序列的第一个,跳过 + continue; + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + int curNum = num; + int curLen = 1; + + while (set.contains(curNum + 1)) { + curNum += 1; + curLen += 1; + } + // 更新最长连续序列的长度 + res = Math.max(res, curLen); + } + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestConsecutive = function(nums) { + // 转化成哈希集合,方便快速查找是否存在某个元素 + const set = new Set(nums); + let res = 0; + + for (const num of set) { + if (set.has(num - 1)) { + // num 不是连续子序列的第一个,跳过 + continue; + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + let curNum = num; + let curLen = 1; + + while (set.has(curNum + 1)) { + curNum += 1; + curLen += 1; + } + // 更新最长连续序列的长度 + res = Math.max(res, curLen); + } + + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def longestConsecutive(self, nums: List[int]) -> int: + # 转化成哈希集合,方便快速查找是否存在某个元素 + num_set = set(nums) + + res = 0 + + for num in num_set: + if num - 1 in num_set: + # num 不是连续子序列的第一个,跳过 + continue + # num 是连续子序列的第一个,开始向上计算连续子序列的长度 + cur_num = num + cur_len = 1 + + while cur_num + 1 in num_set: + cur_num += 1 + cur_len += 1 + # 更新最长连续序列的长度 + res = max(res, cur_len) + + return res +``` + +https://leetcode.cn/problems/WhsWhI 的多语言解法👆 + +https://leetcode.cn/problems/XltzEq 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isPalindrome(string s) { + // 先把所有字符转化成小写,并过滤掉空格和标点这类字符 + string filtered; + for (int i = 0; i < s.length(); i++) { + char c = s[i]; + if (isalnum(c)) { // 使用isalnum函数判断是否为字母或数字 + filtered += tolower(c); // 使用tolower函数将字符转换为小写 + } + } + + // 然后对剩下的这些目标字符执行双指针算法,判断回文串 + s = filtered; + // 一左一右两个指针相向而行 + int left = 0, right = s.length() - 1; + while (left < right) { + if (s[left] != s[right]) { + return false; + } + left++; + right--; + } + return true; + } +}; +``` + +```go +// by chatGPT (go) +func isPalindrome(s string) bool { + // 先把所有字符转化成小写,并过滤掉空格和标点这类字符 + sb := strings.Builder{} + for _, c := range s { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + sb.WriteRune(unicode.ToLower(c)) + } + } + + // 然后对剩下的这些目标字符执行双指针算法,判断回文串 + s = sb.String() + // 一左一右两个指针相向而行 + left, right := 0, len(s)-1 + for left < right { + if s[left] != s[right] { + return false + } + left++ + right-- + } + return true +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isPalindrome(String s) { + // 先把所有字符转化成小写,并过滤掉空格和标点这类字符 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isLetterOrDigit(c)) { + sb.append(Character.toLowerCase(c)); + } + } + + // 然后对剩下的这些目标字符执行双指针算法,判断回文串 + s = sb.toString(); + // 一左一右两个指针相向而行 + int left = 0, right = s.length() - 1; + while (left < right) { + if (s.charAt(left) != s.charAt(right)) { + return false; + } + left++; + right--; + } + return true; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isPalindrome = function(s) { + // 先把所有字符转化成小写,并过滤掉空格和标点这类字符 + let sb = ''; + for (let i = 0; i < s.length; i++) { + let c = s.charAt(i); + if (/[0-9a-zA-Z]/.test(c)) { + sb += c.toLowerCase(); + } + } + + // 然后对剩下的这些目标字符执行双指针算法,判断回文串 + s = sb; + // 一左一右两个指针相向而行 + let left = 0, right = s.length - 1; + while (left < right) { + if (s.charAt(left) !== s.charAt(right)) { + return false; + } + left++; + right--; + } + return true; +} +``` + +```python +# by chatGPT (python) +class Solution: + def isPalindrome(self, s: str) -> bool: + # 先把所有字符转化成小写,并过滤掉空格和标点这类字符 + sb = [] + for c in s: + if c.isalnum(): + sb.append(c.lower()) + + # 然后对剩下的这些目标字符执行双指针算法,判断回文串 + s = ''.join(sb) + # 一左一右两个指针相向而行 + left, right = 0, len(s) - 1 + while left < right: + if s[left] != s[right]: + return False + left += 1 + right -= 1 + return True +``` + +https://leetcode.cn/problems/XltzEq 的多语言解法👆 + +https://leetcode.cn/problems/YaVDxD 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findTargetSumWays(vector& nums, int target) { + if (nums.empty()) return 0; + return dp(nums, 0, target); + } + + // 备忘录 + unordered_map memo; + + int dp(vector& nums, int i, int remain) { + // base case + if (i == nums.size()) { + if (remain == 0) return 1; + return 0; + } + // 把它俩转成字符串才能作为哈希表的键 + string key = to_string(i) + "," + to_string(remain); + // 避免重复计算 + if (memo.count(key)) { + return memo[key]; + } + // 还是穷举 + int result = dp(nums, i + 1, remain - nums[i]) + dp(nums, i + 1, remain + nums[i]); + // 记入备忘录 + memo[key] = result; + return result; + } +}; +``` + +```go +// by chatGPT (go) +func findTargetSumWays(nums []int, target int) int { + if len(nums) == 0 { + return 0 + } + //初始化备忘录 + memo := make(map[string]int) + return dp(nums, 0, target, memo) +} + +func dp(nums []int, i int, remain int, memo map[string]int) int { + //基本情况 + if i == len(nums) { + if remain == 0 { + return 1 + } + return 0 + } + key := fmt.Sprintf("%d,%d", i, remain) + //避免重复计算 + if _, ok := memo[key]; ok { + return memo[key] + } + //穷举 + result := dp(nums, i+1, remain-nums[i], memo) + dp(nums, i+1, remain+nums[i], memo) + //记录结果到备忘录 + memo[key] = result + return result +} +``` + +```java +// by labuladong (java) +class Solution { + public int findTargetSumWays(int[] nums, int target) { + if (nums.length == 0) return 0; + return dp(nums, 0, target); + } + + // 备忘录 + HashMap memo = new HashMap<>(); + + int dp(int[] nums, int i, int remain) { + // base case + if (i == nums.length) { + if (remain == 0) return 1; + return 0; + } + // 把它俩转成字符串才能作为哈希表的键 + String key = i + "," + remain; + // 避免重复计算 + if (memo.containsKey(key)) { + return memo.get(key); + } + // 还是穷举 + int result = dp(nums, i + 1, remain - nums[i]) + dp(nums, i + 1, remain + nums[i]); + // 记入备忘录 + memo.put(key, result); + return result; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findTargetSumWays = function(nums, target) { + if (nums.length === 0) return 0; + // 备忘录 + let memo = new Map(); + + function dp(nums, i, remain) { + // base case + if (i === nums.length) { + if (remain === 0) return 1; + return 0; + } + // 把它俩转成字符串才能作为哈希表的键 + let key = i + "," + remain; + // 避免重复计算 + if (memo.has(key)) { + return memo.get(key); + } + // 还是穷举 + let result = dp(nums, i + 1, remain - nums[i]) + dp(nums, i + 1, remain + nums[i]); + // 记入备忘录 + memo.set(key, result); + return result; + } + + return dp(nums, 0, target); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.memo = {} + + def findTargetSumWays(self, nums: List[int], target: int) -> int: + if len(nums) == 0: + return 0 + return self.dp(nums, 0, target) + + def dp(self, nums: List[int], i: int, remain: int) -> int: + # base case + if i == len(nums): + if remain == 0: + return 1 + return 0 + # 把它俩转成字符串才能作为哈希表的键 + key = str(i) + "," + str(remain) + # 避免重复计算 + if key in self.memo: + return self.memo[key] + # 还是穷举 + result = self.dp(nums, i + 1, remain - nums[i]) + self.dp(nums, i + 1, remain + nums[i]) + # 记入备忘录 + self.memo[key] = result + return result +``` + +https://leetcode.cn/problems/YaVDxD 的多语言解法👆 + +https://leetcode.cn/problems/Ygoe9J 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + vector> res; + list track; + +public: + vector> combinationSum(vector& candidates, int target) { + if (candidates.empty()) { + return res; + } + backtrack(candidates, 0, target, 0); + return res; + } + + // 回溯算法主函数 + void backtrack(vector& candidates, int start, int target, int sum) { + if (sum == target) { + // 找到目标和 + res.push_back(vector(track.begin(), track.end())); + return; + } + + if (sum > target) { + // 超过目标和,直接结束 + return; + } + + // 回溯算法框架 + for (int i = start; i < candidates.size(); i++) { + // 选择 candidates[i] + track.push_back(candidates[i]); + sum += candidates[i]; + // 递归遍历下一层回溯树 + backtrack(candidates, i, target, sum); + // 撤销选择 candidates[i] + sum -= candidates[i]; + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +func combinationSum(candidates []int, target int) [][]int { + res := [][]int{} + if len(candidates) == 0 { + return res + } + backtrack(candidates, []int{}, target, 0, &res) + return res +} + +// 回溯算法框架 +func backtrack(candidates []int, path []int, target, sum int, res *[][]int) { + if sum == target { + // 找到目标和 + *res = append(*res, append([]int{}, path...)) + return + } + if sum > target { + // 超过目标和,直接结束 + return + } + for i := 0; i < len(candidates); i++ { + // 选择 candidates[i] + path = append(path, candidates[i]) + sum += candidates[i] + // 递归遍历下一层回溯树 + backtrack(candidates[i:], path, target, sum, res) + // 撤销选择 candidates[i] + sum -= candidates[i] + path = path[:len(path)-1] + } +} +``` + +```java +// by labuladong (java) +class Solution { + List> res = new LinkedList<>(); + + public List> combinationSum(int[] candidates, int target) { + if (candidates.length == 0) { + return res; + } + backtrack(candidates, 0, target, 0); + return res; + } + + // 记录回溯的路径 + LinkedList track = new LinkedList<>(); + + // 回溯算法主函数 + void backtrack(int[] candidates, int start, int target, int sum) { + if (sum == target) { + // 找到目标和 + res.add(new LinkedList<>(track)); + return; + } + + if (sum > target) { + // 超过目标和,直接结束 + return; + } + + // 回溯算法框架 + for (int i = start; i < candidates.length; i++) { + // 选择 candidates[i] + track.add(candidates[i]); + sum += candidates[i]; + // 递归遍历下一层回溯树 + backtrack(candidates, i, target, sum); + // 撤销选择 candidates[i] + sum -= candidates[i]; + track.removeLast(); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var combinationSum = function(candidates, target) { + var res = []; + if (candidates.length === 0) { + return res; + } + + // 记录回溯的路径 + var track = []; + + // 回溯算法主函数 + var backtrack = function(candidates, start, target, sum) { + if (sum === target) { + // 找到目标和 + res.push([...track]); + return; + } + + if (sum > target) { + // 超过目标和,直接结束 + return; + } + + // 回溯算法框架 + for (var i = start; i < candidates.length; i++) { + // 选择 candidates[i] + track.push(candidates[i]); + sum += candidates[i]; + // 递归遍历下一层回溯树 + backtrack(candidates, i, target, sum); + // 撤销选择 candidates[i] + sum -= candidates[i]; + track.pop(); + } + }; + + backtrack(candidates, 0, target, 0); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + self.track = [] + + def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: + if not candidates: + return self.res + self.backtrack(candidates, 0, target, 0) + return self.res + + # 记录回溯的路径 + def backtrack(self, candidates, start, target, summation): + if summation == target: + # 找到目标和 + self.res.append(self.track[:]) + return + if summation > target: + # 超过目标和,直接结束 + return + # 回溯算法框架 + for i in range(start, len(candidates)): + # 选择 candidates[i] + self.track.append(candidates[i]) + summation += candidates[i] + # 递归遍历下一层回溯树 + self.backtrack(candidates, i, target, summation) + # 撤销选择 candidates[i] + summation -= candidates[i] + self.track.pop() +``` + +https://leetcode.cn/problems/Ygoe9J 的多语言解法👆 + +https://leetcode.cn/problems/ZL6zAn 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxAreaOfIsland(vector>& grid) { + // 记录岛屿的最大面积 + int res = 0; + int m = grid.size(), n = grid[0].size(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹没岛屿,并更新最大岛屿面积 + res = max(res, dfs(grid, i, j)); + } + } + } + return res; + } + + // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + int dfs(vector>& grid, int i, int j) { + int m = grid.size(), n = grid[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] == 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; + + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + } +}; +``` + +```go +// by chatGPT (go) +// 记录岛屿的最大面积 +func maxAreaOfIsland(grid [][]int) int { + res := 0 + m, n := len(grid), len(grid[0]) + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid[i][j] == 1 { + // 淹没岛屿,并更新最大岛屿面积 + res = max(res, dfs(grid, i, j)) + } + } + } + return res +} + +// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 +func dfs(grid [][]int, i, j int) int { + m, n := len(grid), len(grid[0]) + if i < 0 || j < 0 || i >= m || j >= n { + // 超出索引边界 + return 0 + } + if grid[i][j] == 0 { + // 已经是海水了 + return 0 + } + // 将 (i, j) 变成海水 + grid[i][j] = 0 + + return dfs(grid, i+1, j) + + dfs(grid, i, j+1) + + dfs(grid, i-1, j) + + dfs(grid, i, j-1) + 1 +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxAreaOfIsland(int[][] grid) { + // 记录岛屿的最大面积 + int res = 0; + int m = grid.length, n = grid[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹没岛屿,并更新最大岛屿面积 + res = Math.max(res, dfs(grid, i, j)); + } + } + } + return res; + } + + // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + int dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] == 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; + + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxAreaOfIsland = function(grid) { + let res = 0; + let m = grid.length, n = grid[0].length; + + function dfs(grid, i, j) { + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] == 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; + + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + } + + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (grid[i][j] == 1) { + res = Math.max(res, dfs(grid, i, j)); + } + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxAreaOfIsland(self, grid: List[List[int]]) -> int: + # 记录岛屿的最大面积 + res = 0 + m, n = len(grid), len(grid[0]) + for i in range(m): + for j in range(n): + if grid[i][j] == 1: + # 淹没岛屿,并更新最大岛屿面积 + res = max(res, self.dfs(grid, i, j)) + return res + + # 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + def dfs(self, grid: List[List[int]], i: int, j: int) -> int: + m, n = len(grid), len(grid[0]) + if i < 0 or j < 0 or i >= m or j >= n: + # 超出索引边界 + return 0 + if grid[i][j] == 0: + # 已经是海水了 + return 0 + # 将 (i, j) 变成海水 + grid[i][j] = 0 + + return (self.dfs(grid, i + 1, j) + + self.dfs(grid, i, j + 1) + + self.dfs(grid, i - 1, j) + + self.dfs(grid, i, j - 1) + 1) +``` + +https://leetcode.cn/problems/ZL6zAn 的多语言解法👆 + +https://leetcode.cn/problems/aMhZSa 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isPalindrome(ListNode* head) { + ListNode *slow, *fast; + slow = fast = head; + while (fast != nullptr && fast->next != nullptr) { + slow = slow->next; + fast = fast->next->next; + } + + if (fast != nullptr) + slow = slow->next; + + ListNode *left = head; + ListNode *right = reverse(slow); + while (right != nullptr) { + if (left->val != right->val) + return false; + left = left->next; + right = right->next; + } + + return true; + } + + ListNode* reverse(ListNode* head) { + ListNode *pre = nullptr, *cur = head; + while (cur != nullptr) { + ListNode *next = cur->next; + cur->next = pre; + pre = cur; + cur = next; + } + return pre; + } +}; +``` + +```go +// by chatGPT (go) +func isPalindrome(head *ListNode) bool { + slow, fast := head, head + for fast != nil && fast.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + if fast != nil { + slow = slow.Next + } + left, right := head, reverse(slow) + for right != nil { + if left.Val != right.Val { + return false + } + left = left.Next + right = right.Next + } + return true +} + +func reverse(head *ListNode) *ListNode { + var pre *ListNode + cur := head + for cur != nil { + next := cur.Next + cur.Next = pre + pre = cur + cur = next + } + return pre +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isPalindrome(ListNode head) { + ListNode slow, fast; + slow = fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + if (fast != null) + slow = slow.next; + + ListNode left = head; + ListNode right = reverse(slow); + while (right != null) { + if (left.val != right.val) + return false; + left = left.next; + right = right.next; + } + + return true; + } + + ListNode reverse(ListNode head) { + ListNode pre = null, cur = head; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isPalindrome = function(head) { + let slow, fast; + slow = fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + if (fast != null) + slow = slow.next; + + let left = head; + let right = reverse(slow); + while (right != null) { + if (left.val != right.val) + return false; + left = left.next; + right = right.next; + } + + return true; +}; + +var reverse = function(head) { + let pre = null, cur = head; + while (cur != null) { + let next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isPalindrome(self, head: ListNode) -> bool: + slow, fast = head, head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + + if fast: + slow = slow.next + + left = head + right = self.reverse(slow) + while right: + if left.val != right.val: + return False + left = left.next + right = right.next + + return True + + def reverse(self, head: ListNode) -> ListNode: + pre, cur = None, head + while cur: + nxt = cur.next + cur.next = pre + pre = cur + cur = nxt + return pre +``` + +https://leetcode.cn/problems/aMhZSa 的多语言解法👆 + +https://leetcode.cn/problems/add-binary 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string addBinary(string a, string b) { + // 先把输入的这两个二进制串反转,低位放在前面,方便处理进位 + reverse(a.begin(), a.end()); + reverse(b.begin(), b.end()); + // 存储结果 + string res = ""; + + int m = a.size(), n = b.size(); + // carry 记录进位 + int carry = 0; + int i = 0; + + // 开始类似 [2. 两数相加](#2) 的加法模拟逻辑 + // 只是这里运算的是二进制字符串 + while (i < max(m, n) || carry > 0) { + int val = carry; + val += i < m ? (a[i] - '0') : 0; + val += i < n ? (b[i] - '0') : 0; + res.push_back(val % 2 + '0'); + carry = val / 2; + i++; + } + + reverse(res.begin(), res.end()); + return res; + } +}; +``` + +```go +// by chatGPT (go) +func addBinary(a string, b string) string { + // 先把输入的这两个二进制串反转,低位放在前面,方便处理进位 + reverse := func(str string) string { + reversed := []rune(str) + for i, j := 0, len(reversed)-1; i < j; i, j = i+1, j-1 { + reversed[i], reversed[j] = reversed[j], reversed[i] + } + return string(reversed) + } + a = reverse(a) + b = reverse(b) + // 存储结果 + var sb strings.Builder + + m, n := len(a), len(b) + // carry 记录进位 + carry := 0 + i := 0 + + // 开始类似 [2. 两数相加](#2) 的加法模拟逻辑 + // 只是这里运算的是二进制字符串 + for i < max(m, n) || carry > 0 { + val := carry + if i < m { + val += int(a[i] - '0') + } + if i < n { + val += int(b[i] - '0') + } + sb.WriteString(strconv.Itoa(val % 2)) + carry = val / 2 + i++ + } + + return reverse(sb.String()) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public String addBinary(String a, String b) { + // 先把输入的这两个二进制串反转,低位放在前面,方便处理进位 + a = new StringBuilder(a).reverse().toString(); + b = new StringBuilder(b).reverse().toString(); + // 存储结果 + StringBuilder sb = new StringBuilder(); + + int m = a.length(), n = b.length(); + // carry 记录进位 + int carry = 0; + int i = 0; + + // 开始类似 [2. 两数相加](#2) 的加法模拟逻辑 + // 只是这里运算的是二进制字符串 + while (i < Math.max(m, n) || carry > 0) { + int val = carry; + val += i < m ? (a.charAt(i) - '0') : 0; + val += i < n ? (b.charAt(i) - '0') : 0; + sb.append(val % 2); + carry = val / 2; + i++; + } + + return sb.reverse().toString(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var addBinary = function(a, b) { + // 先把输入的这两个二进制串反转,低位放在前面,方便处理进位 + a = a.split("").reverse().join(""); + b = b.split("").reverse().join(""); + // 存储结果 + var sb = ""; + + var m = a.length, n = b.length; + // carry 记录进位 + var carry = 0; + var i = 0; + + // 开始类似 [2. 两数相加](#2) 的加法模拟逻辑 + // 只是这里运算的是二进制字符串 + while (i < Math.max(m, n) || carry > 0) { + var val = carry; + val += i < m ? parseInt(a.charAt(i)) : 0; + val += i < n ? parseInt(b.charAt(i)) : 0; + sb += (val % 2); + carry = Math.floor(val / 2); + i++; + } + + return sb.split("").reverse().join(""); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def addBinary(self, a: str, b: str) -> str: + # 先把输入的这两个二进制串反转,低位放在前面,方便处理进位 + a = a[::-1] + b = b[::-1] + # 存储结果 + sb = [] + + m, n = len(a), len(b) + # carry 记录进位 + carry = 0 + i = 0 + + # 开始类似 [2. 两数相加](#2) 的加法模拟逻辑 + # 只是这里运算的是二进制字符串 + while i < max(m, n) or carry > 0: + val = carry + val += int(a[i]) if i < m else 0 + val += int(b[i]) if i < n else 0 + sb.append(str(val % 2)) + carry = val // 2 + i += 1 + + return "".join(sb[::-1]) +``` + +https://leetcode.cn/problems/add-binary 的多语言解法👆 + +https://leetcode.cn/problems/add-two-numbers 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { + // 在两条链表上的指针 + ListNode *p1 = l1, *p2 = l2; + // 虚拟头结点(构建新链表时的常用技巧) + ListNode *dummy = new ListNode(-1); + // 指针 p 负责构建新链表 + ListNode *p = dummy; + // 记录进位 + int carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (p1 != nullptr || p2 != nullptr || carry > 0) { + // 先加上上次的进位 + int val = carry; + if (p1 != nullptr) { + val += p1->val; + p1 = p1->next; + } + if (p2 != nullptr) { + val += p2->val; + p2 = p2->next; + } + // 处理进位情况 + carry = val / 10; + val = val % 10; + // 构建新节点 + p->next = new ListNode(val); + p = p->next; + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy->next; + } +}; +``` + +```go +// by chatGPT (go) +func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode { + // 在两条链表上的指针 + p1, p2 := l1, l2 + // 虚拟头结点(构建新链表时的常用技巧) + dummy := &ListNode{-1, nil} + // 指针 p 负责构建新链表 + p := dummy + // 记录进位 + carry := 0 + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + for p1 != nil || p2 != nil || carry > 0 { + // 先加上上次的进位 + val := carry + if p1 != nil { + val += p1.Val + p1 = p1.Next + } + if p2 != nil { + val += p2.Val + p2 = p2.Next + } + // 处理进位情况 + carry = val / 10 + val = val % 10 + // 构建新节点 + p.Next = &ListNode{val, nil} + p = p.Next + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.Next +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + // 在两条链表上的指针 + ListNode p1 = l1, p2 = l2; + // 虚拟头结点(构建新链表时的常用技巧) + ListNode dummy = new ListNode(-1); + // 指针 p 负责构建新链表 + ListNode p = dummy; + // 记录进位 + int carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (p1 != null || p2 != null || carry > 0) { + // 先加上上次的进位 + int val = carry; + if (p1 != null) { + val += p1.val; + p1 = p1.next; + } + if (p2 != null) { + val += p2.val; + p2 = p2.next; + } + // 处理进位情况 + carry = val / 10; + val = val % 10; + // 构建新节点 + p.next = new ListNode(val); + p = p.next; + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +var addTwoNumbers = function(l1, l2) { + // 在两条链表上的指针 + let p1 = l1, p2 = l2; + // 虚拟头结点(构建新链表时的常用技巧) + let dummy = new ListNode(-1); + // 指针 p 负责构建新链表 + let p = dummy; + // 记录进位 + let carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (p1 !== null || p2 !== null || carry > 0) { + // 先加上上次的进位 + let val = carry; + if (p1 !== null) { + val += p1.val; + p1 = p1.next; + } + if (p2 !== null) { + val += p2.val; + p2 = p2.next; + } + // 处理进位情况 + carry = Math.floor(val / 10); + val = val % 10; + // 构建新节点 + p.next = new ListNode(val); + p = p.next; + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.next; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: + # 在两条链表上的指针 + p1, p2 = l1, l2 + # 虚拟头结点(构建新链表时的常用技巧) + dummy = ListNode(-1) + # 指针 p 负责构建新链表 + p = dummy + # 记录进位 + carry = 0 + # 开始执行加法,两条链表走完且没有进位时才能结束循环 + while p1 or p2 or carry: + # 先加上上次的进位 + val = carry + if p1: + val += p1.val + p1 = p1.next + if p2: + val += p2.val + p2 = p2.next + # 处理进位情况 + carry, val = divmod(val, 10) + # 构建新节点 + p.next = ListNode(val) + p = p.next + # 返回结果链表的头结点(去除虚拟头结点) + return dummy.next +``` + +https://leetcode.cn/problems/add-two-numbers 的多语言解法👆 + +https://leetcode.cn/problems/add-two-numbers-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { + // 把链表元素转入栈中 + stack stk1, stk2; + while (l1 != nullptr) { + stk1.push(l1->val); + l1 = l1->next; + } + while (l2 != nullptr) { + stk2.push(l2->val); + l2 = l2->next; + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + ListNode* dummy = new ListNode(-1); + + // 记录进位 + int carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (!stk1.empty() || !stk2.empty() || carry > 0) { + // 先加上上次的进位 + int val = carry; + if (!stk1.empty()) { + val += stk1.top(); + stk1.pop(); + } + if (!stk2.empty()) { + val += stk2.top(); + stk2.pop(); + } + // 处理进位情况 + carry = val / 10; + val = val % 10; + // 构建新节点,直接接在 dummy 后面 + ListNode* newNode = new ListNode(val); + newNode->next = dummy->next; + dummy->next = newNode; + } + // 返回结果链表的头结点(去除虚拟头结点) + ListNode* result = dummy->next; + delete dummy; + return result; + } +}; +``` + +```go +// by chatGPT (go) +func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode { + // 把链表元素转入栈中 + stk1 := []int{} + for l1 != nil { + stk1 = append(stk1, l1.Val) + l1 = l1.Next + } + stk2 := []int{} + for l2 != nil { + stk2 = append(stk2, l2.Val) + l2 = l2.Next + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + dummy := &ListNode{-1, nil} + + // 记录进位 + carry := 0 + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + for len(stk1) > 0 || len(stk2) > 0 || carry > 0 { + // 先加上上次的进位 + val := carry + if len(stk1) > 0 { + val += stk1[len(stk1)-1] + stk1 = stk1[:len(stk1)-1] + } + if len(stk2) > 0 { + val += stk2[len(stk2)-1] + stk2 = stk2[:len(stk2)-1] + } + // 处理进位情况 + carry = val / 10 + val = val % 10 + // 构建新节点,直接接在 dummy 后面 + newNode := &ListNode{val, dummy.Next} + dummy.Next = newNode + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.Next +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + // 把链表元素转入栈中 + Stack stk1 = new Stack<>(); + while (l1 != null) { + stk1.push(l1.val); + l1 = l1.next; + } + Stack stk2 = new Stack<>(); + while (l2 != null) { + stk2.push(l2.val); + l2 = l2.next; + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + ListNode dummy = new ListNode(-1); + + // 记录进位 + int carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (!stk1.isEmpty() || !stk2.isEmpty() || carry > 0) { + // 先加上上次的进位 + int val = carry; + if (!stk1.isEmpty()) { + val += stk1.pop(); + } + if (!stk2.isEmpty()) { + val += stk2.pop(); + } + // 处理进位情况 + carry = val / 10; + val = val % 10; + // 构建新节点,直接接在 dummy 后面 + ListNode newNode = new ListNode(val); + newNode.next = dummy.next; + dummy.next = newNode; + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +var addTwoNumbers = function(l1, l2) { + // 把链表元素转入栈中 + const stk1 = []; + while (l1 !== null) { + stk1.push(l1.val); + l1 = l1.next; + } + const stk2 = []; + while (l2 !== null) { + stk2.push(l2.val); + l2 = l2.next; + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + const dummy = new ListNode(-1); + + // 记录进位 + let carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (stk1.length || stk2.length || carry > 0) { + // 先加上上次的进位 + let val = carry; + if (stk1.length) { + val += stk1.pop(); + } + if (stk2.length) { + val += stk2.pop(); + } + // 处理进位情况 + carry = Math.floor(val / 10); + val = val % 10; + // 构建新节点,直接接在 dummy 后面 + const newNode = new ListNode(val); + newNode.next = dummy.next; + dummy.next = newNode; + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.next; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: + # 把链表元素转入栈中 + stk1 = [] + while l1: + stk1.append(l1.val) + l1 = l1.next + stk2 = [] + while l2: + stk2.append(l2.val) + l2 = l2.next + + # 接下来基本上是复用我在第 2 题的代码逻辑 + # 注意新节点要直接插入到 dummy 后面 + + # 虚拟头结点(构建新链表时的常用技巧) + dummy = ListNode(-1) + + # 记录进位 + carry = 0 + # 开始执行加法,两条链表走完且没有进位时才能结束循环 + while stk1 or stk2 or carry > 0: + # 先加上上次的进位 + val = carry + if stk1: + val += stk1.pop() + if stk2: + val += stk2.pop() + # 处理进位情况 + carry = val // 10 + val = val % 10 + # 构建新节点,直接接在 dummy 后面 + newNode = ListNode(val) + newNode.next = dummy.next + dummy.next = newNode + # 返回结果链表的头结点(去除虚拟头结点) + return dummy.next +``` + +https://leetcode.cn/problems/add-two-numbers-ii 的多语言解法👆 + +https://leetcode.cn/problems/additive-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isAdditiveNumber(string num) { + // 穷举前两个数字 + int n = num.size(); + for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + string first = num.substr(0, i); + string second = num.substr(i, j-i); + if (isValid(num, first, second)) { + return true; + } + } + } + return false; + } + +private: + // 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 + bool isValid(string num, string first, string second) { + if ((first[0] == '0' && first.size() > 1) + || (second[0] == '0' && second.size() > 1)) { + // 0 开头的数字,只能是 0 本身 + return false; + } + string sumStr = strAdd(first, second); + string next = num.substr(first.size() + second.size()); + if (next.find(sumStr) != 0) { + // 不满足累加数的性质 + return false; + } + if (next == sumStr) { + // 已经匹配完整个字符串 + return true; + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num.substr(first.size()), second, sumStr); + } + + // 模拟加法竖式运算,具体可以看下这道题 + // https://leetcode-cn.com/problems/add-strings/ + string strAdd(string a, string b) { + int n = a.size(), m = b.size(); + int i = n - 1, j = m - 1, add = 0; + string builder; + while (i >= 0 || j >= 0 || add != 0) { + int x = i >= 0 ? a[i] - '0' : 0; + int y = j >= 0 ? b[j] - '0' : 0; + int result = x + y + add; + builder.push_back(result % 10 + '0'); + add = result / 10; + i--; + j--; + } + reverse(builder.begin(), builder.end()); + return builder; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "strconv" + "strings" +) + +// isAdditiveNumber - 穷举前两个数字 +func isAdditiveNumber(num string) bool { + n := len(num) + for i := 1; i <= n; i++ { + for j := i + 1; j <= n; j++ { + first := num[:i] + second := num[i:j] + if isValid(num, first, second) { + return true + } + } + } + return false +} + +// isValid - 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 +func isValid(num string, first string, second string) bool { + if (strings.HasPrefix(first, "0") && len(first) > 1) || + (strings.HasPrefix(second, "0") && len(second) > 1) { + // 0 开头的数字,只能是 0 本身 + return false + } + sumStr := strAdd(first, second) + next := num[len(first)+len(second):] + if !strings.HasPrefix(next, sumStr) { + // 不满足累加数的性质 + return false + } + if next == sumStr { + // 已经匹配完整个字符串 + return true + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num[len(first):], second, sumStr) +} + +// strAdd - 模拟加法竖式运算,具体可以看下这道题 +// https://leetcode.cn/problems/add-strings/ +func strAdd(a, b string) string { + i, j, carry := len(a)-1, len(b)-1, 0 + ans := "" + for i >= 0 || j >= 0 { + x, _ := strconv.Atoi(string(a[i])) + y, _ := strconv.Atoi(string(b[j])) + sum := x + y + carry + tmp := sum % 10 + carry = sum / 10 + ans = strconv.Itoa(tmp) + ans + i-- + j-- + } + if carry > 0 { + ans = "1" + ans + } + return ans +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isAdditiveNumber(String num) { + // 穷举前两个数字 + int n = num.length(); + for (int i = 1; i <= n; i++) { + // 先穷举第一个数字 + String first = num.substring(0, i); + for (int j = i + 1; j <= n; j++) { + // 再穷举第二个数字 + String second = num.substring(i, j); + if (isValid(num, first, second)) { + return true; + } + } + } + return false; + } + + // 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 + boolean isValid(String num, String first, String second) { + if (first.startsWith("0") && first.length() > 1 + || second.startsWith("0") && second.length() > 1) { + // 0 开头的数字,只能是 0 本身 + return false; + } + String sumStr = strAdd(first, second); + String next = num.substring(first.length() + second.length()); + if (!next.startsWith(sumStr)) { + // 不满足累加数的性质 + return false; + } + if (next.equals(sumStr)) { + // 已经匹配完整个字符串 + return true; + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num.substring(first.length()), second, sumStr); + } + + // 模拟加法竖式运算,具体可以看下这道题 + // https://leetcode.cn/problems/add-strings/ + String strAdd(String a, String b) { + int n = a.length(), m = b.length(); + int i = n - 1, j = m - 1, add = 0; + StringBuilder builder = new StringBuilder(); + while (i >= 0 || j >= 0 || add != 0) { + int x = i >= 0 ? a.charAt(i) - '0' : 0; + int y = j >= 0 ? b.charAt(j) - '0' : 0; + int result = x + y + add; + builder.append(result % 10); + add = result / 10; + i--; + j--; + } + return builder.reverse().toString(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var isAdditiveNumber = function(num) { + // 穷举前两个数字 + let n = num.length; + for (let i = 1; i <= n; i++) { + for (let j = i + 1; j <= n; j++) { + let first = num.substring(0, i); + let second = num.substring(i, j); + if (isValid(num, first, second)) { + return true; + } + } + } + return false; +} + +var isValid = function(num, first, second) { + if ((first.startsWith("0") && first.length > 1) + || (second.startsWith("0") && second.length > 1)) { + // 0 开头的数字,只能是 0 本身 + return false; + } + let sumStr = strAdd(first, second); + let next = num.substring(first.length + second.length); + if (!next.startsWith(sumStr)) { + // 不满足累加数的性质 + return false; + } + if (next === sumStr) { + // 已经匹配完整个字符串 + return true; + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num.substring(first.length), second, sumStr); +} + +var strAdd = function(a, b) { + let n = a.length, m = b.length; + let i = n - 1, j = m - 1, add = 0; + let builder = []; + while (i >= 0 || j >= 0 || add != 0) { + let x = i >= 0 ? a.charAt(i) - '0' : 0; + let y = j >= 0 ? b.charAt(j) - '0' : 0; + let result = x + y + add; + builder.push(result % 10); + add = Math.floor(result / 10); + i--; + j--; + } + return builder.reverse().join(''); +} +``` + +```python +# by chatGPT (python) +class Solution: + def isAdditiveNumber(self, num): + # 穷举前两个数字 + n = len(num) + for i in range(1, n + 1): + for j in range(i + 1, n + 1): + first = num[0 : i] + second = num[i : j] + if self.isValid(num, first, second): + return True + return False + + def isValid(self, num, first, second): + # 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 + if (first.startswith("0") and len(first) > 1) or (second.startswith("0") and len(second) > 1): + # 0 开头的数字,只能是 0 本身 + return False + sumStr = self.strAdd(first, second) + next = num[len(first) + len(second):] + if not next.startswith(sumStr): + # 不满足累加数的性质 + return False + if next == sumStr: + # 已经匹配完整个字符串 + return True + # 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + # 你也可以改用迭代写法,一样的 + return self.isValid(num[len(first):], second, sumStr) + + def strAdd(self, a, b): + # 模拟加法竖式运算,具体可以看下这道题 + # https://leetcode.cn/problems/add-strings/ + n, m = len(a), len(b) + i, j, add = n - 1, m - 1, 0 + builder = [] + while i >= 0 or j >= 0 or add != 0: + x = int(a[i]) if i >= 0 else 0 + y = int(b[j]) if j >= 0 else 0 + result = x + y + add + builder.append(result % 10) + add = result // 10 + i -= 1 + j -= 1 + return ''.join(str(i) for i in builder[::-1]) +``` + +https://leetcode.cn/problems/additive-number 的多语言解法👆 + +https://leetcode.cn/problems/advantage-shuffle 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector advantageCount(vector& nums1, vector& nums2) { + int n = nums1.size(); + // 给 nums2 降序排序 + priority_queue> maxpq; + for (int i = 0; i < n; i++) { + maxpq.push({i, nums2[i]}); + } + // 给 nums1 升序排序 + sort(nums1.begin(), nums1.end()); + + // nums1[left] 是最小值,nums1[right] 是最大值 + int left = 0, right = n - 1; + vector res(n); + + while (!maxpq.empty()) { + auto [i, maxval] = maxpq.top(); maxpq.pop(); + // maxval 是 nums2 中的最大值,i 是对应索引 + if (maxval < nums1[right]) { + // 如果 nums1[right] 能胜过 maxval,那就自己上 + res[i] = nums1[right]; + right--; + } else { + // 否则用最小值混一下,养精蓄锐 + res[i] = nums1[left]; + left++; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func advantageCount(nums1 []int, nums2 []int) []int { + n := len(nums1) + // 给 nums2 降序排序 + maxpq := make(PriorityQueue, 0) + heap.Init(&maxpq) + for i := 0; i < n; i++ { + heap.Push(&maxpq, []int{i, nums2[i]}) + } + // 给 nums1 升序排序 + sort.Ints(nums1) + + // nums1[left] 是最小值,nums1[right] 是最大值 + left, right := 0, n-1 + res := make([]int, n) + + for maxpq.Len() > 0 { + pair := heap.Pop(&maxpq).([]int) + // maxval 是 nums2 中的最大值,i 是对应索引 + i, maxval := pair[0], pair[1] + if maxval < nums1[right] { + // 如果 nums1[right] 能胜过 maxval,那就自己上 + res[i] = nums1[right] + right-- + } else { + // 否则用最小值混一下,养精蓄锐 + res[i] = nums1[left] + left++ + } + } + return res +} + +// 定义一个优先队列类型 PriorityQueue,用于按照指定比较函数排序 +type PriorityQueue [][]int + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i][1] > pq[j][1] +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.([]int) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] advantageCount(int[] nums1, int[] nums2) { + int n = nums1.length; + // 给 nums2 降序排序 + PriorityQueue maxpq = new PriorityQueue<>( + (int[] pair1, int[] pair2) -> { + return pair2[1] - pair1[1]; + } + ); + for (int i = 0; i < n; i++) { + maxpq.offer(new int[]{i, nums2[i]}); + } + // 给 nums1 升序排序 + Arrays.sort(nums1); + + // nums1[left] 是最小值,nums1[right] 是最大值 + int left = 0, right = n - 1; + int[] res = new int[n]; + + while (!maxpq.isEmpty()) { + int[] pair = maxpq.poll(); + // maxval 是 nums2 中的最大值,i 是对应索引 + int i = pair[0], maxval = pair[1]; + if (maxval < nums1[right]) { + // 如果 nums1[right] 能胜过 maxval,那就自己上 + res[i] = nums1[right]; + right--; + } else { + // 否则用最小值混一下,养精蓄锐 + res[i] = nums1[left]; + left++; + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var advantageCount = function(nums1, nums2) { + var n = nums1.length; + //给 nums2 降序排序 + var maxpq = new PriorityQueue((pair1, pair2) => pair2[1] - pair1[1]); + for (var i = 0; i < n; i++) { + maxpq.offer([i, nums2[i]]); + } + //给 nums1 升序排序 + nums1.sort((a, b) => a - b); + // nums1[left] 是最小值,nums1[right] 是最大值 + var left = 0, right = n - 1; + var res = new Array(n); + + while (!maxpq.isEmpty()) { + var pair = maxpq.poll(); + // maxval 是 nums2 中的最大值,i 是对应索引 + var i = pair[0], maxval = pair[1]; + if (maxval < nums1[right]) { + // 如果 nums1[right] 能胜过 maxval,那就自己上 + res[i] = nums1[right]; + right--; + } else { + // 否则用最小值混一下,养精蓄锐 + res[i] = nums1[left]; + left++; + } + } + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]: + n = len(nums1) + # 给 nums2 降序排序 + maxpq = [] + for i in range(n): + maxpq.append([i, nums2[i]]) + maxpq.sort(key=lambda x: -x[1]) + + # 给 nums1 升序排序 + nums1.sort() + + # nums1[left] 是最小值,nums1[right] 是最大值 + left, right = 0, n - 1 + res = [0] * n + + while maxpq: + pair = maxpq.pop(0) + # maxval 是 nums2 中的最大值,i 是对应索引 + i, maxval = pair[0], pair[1] + if maxval < nums1[right]: + # 如果 nums1[right] 能胜过 maxval,那就自己上 + res[i] = nums1[right] + right -= 1 + else: + # 否则用最小值混一下,养精蓄锐 + res[i] = nums1[left] + left += 1 + return res +``` + +https://leetcode.cn/problems/advantage-shuffle 的多语言解法👆 + +https://leetcode.cn/problems/all-paths-from-source-to-target 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 记录所有路径 + vector> res; + +public: + vector> allPathsSourceTarget(vector>& graph) { + deque path; + traverse(graph, 0, path); + return res; + } + + /* 图的遍历框架 */ + void traverse(vector>& graph, int s, deque& path) { + + // 添加节点 s 到路径 + path.push_back(s); + + int n = graph.size(); + if (s == n - 1) { + // 到达终点 + res.push_back(vector(path.begin(), path.end())); + path.pop_back(); + return; + } + + // 递归每个相邻节点 + for (int v : graph[s]) { + traverse(graph, v, path); + } + + // 从路径移出节点 s + path.pop_back(); + } +}; +``` + +```go +// by chatGPT (go) +func allPathsSourceTarget(graph [][]int) [][]int { + res := [][]int{} + path := []int{} + + var traverse func(graph [][]int, s int, path []int) + traverse = func(graph [][]int, s int, path []int) { + // 添加节点 s 到路径 + path = append(path, s) + + n := len(graph) + if s == n - 1 { + // 到达终点 + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + path = path[:len(path) - 1] + return + } + + // 递归每个相邻节点 + for _, v := range graph[s] { + traverse(graph, v, path) + } + + // 从路径移出节点 s + path = path[:len(path) - 1] + } + + traverse(graph, 0, path) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + // 记录所有路径 + List> res = new LinkedList<>(); + + public List> allPathsSourceTarget(int[][] graph) { + LinkedList path = new LinkedList<>(); + traverse(graph, 0, path); + return res; + } + + /* 图的遍历框架 */ + void traverse(int[][] graph, int s, LinkedList path) { + + // 添加节点 s 到路径 + path.addLast(s); + + int n = graph.length; + if (s == n - 1) { + // 到达终点 + res.add(new LinkedList<>(path)); + path.removeLast(); + return; + } + + // 递归每个相邻节点 + for (int v : graph[s]) { + traverse(graph, v, path); + } + + // 从路径移出节点 s + path.removeLast(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var allPathsSourceTarget = function(graph) { + // 记录所有路径 + var res = []; + + var traverse = function(graph, s, path) { + // 添加节点 s 到路径 + path.push(s); + + var n = graph.length; + if (s === n - 1) { + // 到达终点 + res.push(path.slice()); + path.pop(); + return; + } + + // 递归每个相邻节点 + for (var i = 0; i < graph[s].length; i++) { + traverse(graph, graph[s][i], path); + } + + // 从路径移出节点 s + path.pop(); + }; + + var path = []; + traverse(graph, 0, path); + + return res; +}; +``` + +```python +# by chatGPT (python) +from typing import List + +class Solution: + def __init__(self): + # 记录所有路径 + self.res = [] + + def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]: + path = [] + self.traverse(graph, 0, path) + return self.res + + """ 图的遍历框架 """ + def traverse(self, graph: List[List[int]], s: int, path: List[int]) -> None: + # 添加节点 s 到路径 + path.append(s) + + n = len(graph) + if s == n - 1: + # 到达终点 + self.res.append(path[:]) + path.pop() + return + + # 递归每个相邻节点 + for v in graph[s]: + self.traverse(graph, v, path) + + # 从路径移出节点 s + path.pop() +``` + +https://leetcode.cn/problems/all-paths-from-source-to-target 的多语言解法👆 + +https://leetcode.cn/problems/bP4bmD 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 记录所有路径 + vector> res; + +public: + vector> allPathsSourceTarget(vector>& graph) { + deque path; + traverse(graph, 0, path); + return res; + } + + /* 图的遍历框架 */ + void traverse(vector>& graph, int s, deque& path) { + + // 添加节点 s 到路径 + path.push_back(s); + + int n = graph.size(); + if (s == n - 1) { + // 到达终点 + res.push_back(vector(path.begin(), path.end())); + path.pop_back(); + return; + } + + // 递归每个相邻节点 + for (int v : graph[s]) { + traverse(graph, v, path); + } + + // 从路径移出节点 s + path.pop_back(); + } +}; +``` + +```go +// by chatGPT (go) +func allPathsSourceTarget(graph [][]int) [][]int { + // 记录所有路径 + res := [][]int{} + var traverse func(graph [][]int, s int, path []int) + + traverse = func(graph [][]int, s int, path []int) { + // 添加节点 s 到路径 + path = append(path, s) + + n := len(graph) + if s == n-1 { + // 到达终点 + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + path = path[:len(path)-1] + return + } + + // 递归每个相邻节点 + for _, v := range graph[s] { + traverse(graph, v, path) + } + + // 从路径移出节点 s + path = path[:len(path)-1] + } + + path := make([]int, 0) + traverse(graph, 0, path) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + // 记录所有路径 + List> res = new LinkedList<>(); + + public List> allPathsSourceTarget(int[][] graph) { + LinkedList path = new LinkedList<>(); + traverse(graph, 0, path); + return res; + } + + /* 图的遍历框架 */ + void traverse(int[][] graph, int s, LinkedList path) { + + // 添加节点 s 到路径 + path.addLast(s); + + int n = graph.length; + if (s == n - 1) { + // 到达终点 + res.add(new LinkedList<>(path)); + path.removeLast(); + return; + } + + // 递归每个相邻节点 + for (int v : graph[s]) { + traverse(graph, v, path); + } + + // 从路径移出节点 s + path.removeLast(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var allPathsSourceTarget = function(graph) { + // 记录所有路径 + let res = []; + + let traverse = function(graph, s, path) { + // 添加节点 s 到路径 + path.push(s); + + let n = graph.length; + if (s == n - 1) { + // 到达终点 + res.push([...path]); + path.pop(); + return; + } + + // 递归每个相邻节点 + for (let v of graph[s]) { + traverse(graph, v, path); + } + + // 从路径移出节点 s + path.pop(); + }; + + let path = []; + traverse(graph, 0, path); + + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + # 记录所有路径 + res = [] + + def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]: + path = [] + self.traverse(graph, 0, path) + return self.res + + # 图的遍历框架 + def traverse(self, graph: List[List[int]], s: int, path: List[int]) -> None: + + # 添加节点 s 到路径 + path.append(s) + + n = len(graph) + if s == n - 1: + # 到达终点 + self.res.append(path[:]) + path.pop() + return + + # 递归每个相邻节点 + for v in graph[s]: + self.traverse(graph, v, path) + + # 从路径移出节点 s + path.pop() +``` + +https://leetcode.cn/problems/bP4bmD 的多语言解法👆 + +https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int translateNum(int num) { + string s = to_string(num); + int n = s.length(); + if (n < 1) { + return 0; + } + // 定义:dp[i] 表示 s[0..i-1] 的解码方式数量 + vector dp(n + 1); + // base case: s 为空或者 s 只有一个字符的情况 + dp[0] = 1; + dp[1] = 1; + + // 注意 dp 数组和 s 之间的索引偏移一位 + for (int i = 2; i <= n; i++) { + char c = s[i - 1], d = s[i - 2]; + if ('0' <= c && c <= '9') { + // 1. s[i] 本身可以作为一个字母 + dp[i] += dp[i - 1]; + } + if (d == '1' || d == '2' && c <= '5') { + // 2. s[i] 和 s[i - 1] 结合起来表示一个字母 + dp[i] += dp[i - 2]; + } + } + return dp[n]; + } +}; +``` + +```go +// by chatGPT (go) +func translateNum(num int) int { + s := strconv.Itoa(num) + n := len(s) + if n < 1 { + return 0 + } + // 定义:dp[i] 表示 s[0..i-1] 的解码方式数量 + dp := make([]int, n+1) + // base case: s 为空或者 s 只有一个字符的情况 + dp[0] = 1 + dp[1] = 1 + + // 注意 dp 数组和 s 之间的索引偏移一位 + for i := 2; i <= n; i++ { + c, d := s[i-1], s[i-2] + if '0' <= c && c <= '9' { + // 1. s[i] 本身可以作为一个字母 + dp[i] += dp[i-1] + } + if (d == '1' || (d == '2' && c <= '5')) { + // 2. s[i] 和 s[i - 1] 结合起来表示一个字母 + dp[i] += dp[i-2] + } + } + return dp[n] +} +``` + +```java +// by labuladong (java) +class Solution { + public int translateNum(int num) { + String s = num + ""; + int n = s.length(); + if (n < 1) { + return 0; + } + // 定义:dp[i] 表示 s[0..i-1] 的解码方式数量 + int[] dp = new int[n + 1]; + // base case: s 为空或者 s 只有一个字符的情况 + dp[0] = 1; + dp[1] = 1; + + // 注意 dp 数组和 s 之间的索引偏移一位 + for (int i = 2; i <= n; i++) { + char c = s.charAt(i - 1), d = s.charAt(i - 2); + if ('0' <= c && c <= '9') { + // 1. s[i] 本身可以作为一个字母 + dp[i] += dp[i - 1]; + } + if (d == '1' || d == '2' && c <= '5') { + // 2. s[i] 和 s[i - 1] 结合起来表示一个字母 + dp[i] += dp[i - 2]; + } + } + return dp[n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var translateNum = function(num) { + var s = num.toString(); + var n = s.length; + if (n < 1) { + return 0; + } + // 定义:dp[i] 表示 s[0..i-1] 的解码方式数量 + var dp = new Array(n + 1).fill(0); + // base case: s 为空或者 s 只有一个字符的情况 + dp[0] = 1; + dp[1] = 1; + + // 注意 dp 数组和 s 之间的索引偏移一位 + for (var i = 2; i <= n; i++) { + var c = s.charAt(i - 1), d = s.charAt(i - 2); + if ('0' <= c && c <= '9') { + // 1. s[i] 本身可以作为一个字母 + dp[i] += dp[i - 1]; + } + if (d == '1' || d == '2' && c <= '5') { + // 2. s[i] 和 s[i - 1] 结合起来表示一个字母 + dp[i] += dp[i - 2]; + } + } + return dp[n]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def translateNum(self, num: int) -> int: + s = str(num) + n = len(s) + if n < 1: + return 0 + # 定义:dp[i] 表示 s[0..i-1] 的解码方式数量 + dp = [0] * (n + 1) + # base case: s 为空或者 s 只有一个字符的情况 + dp[0] = 1 + dp[1] = 1 + + # 注意 dp 数组和 s 之间的索引偏移一位 + for i in range(2, n + 1): + c = s[i - 1] + d = s[i - 2] + if '0' <= c <= '9': + # 1. s[i] 本身可以作为一个字母 + dp[i] += dp[i - 1] + if d == '1' or (d == '2' and c <= '5'): + # 2. s[i] 和 s[i - 1] 结合起来表示一个字母 + dp[i] += dp[i - 2] + return dp[n] +``` + +https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof 的多语言解法👆 + +https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string minNumber(vector& nums) { + int n = nums.size(); + vector strs(n); + for (int i = 0; i < n; i++) { + strs[i] = to_string(nums[i]); + } + sort(strs.begin(), strs.end(), [](const string& s1, const string& s2) { + // 看看那种拼接方式得到的数字更小,排前面 + // 不用转成 int 类型,因为字符串的比较算法和正整数的比较算法是一样的 + // 而且拼接字符串比较长,会导致 int 类型溢出 + return (s1 + s2) < (s2 + s1); + }); + + return accumulate(strs.begin(), strs.end(), string("")); + } +}; +``` + +```go +// by chatGPT (go) +func minNumber(nums []int) string { + n := len(nums) + strs := make([]string, n) + for i := 0; i < n; i++ { + strs[i] = strconv.Itoa(nums[i]) + } + sort.Slice(strs, func(i, j int) bool { + // 看看那种拼接方式得到的数字更小,排前面 + // 不用转成 int 类型,因为字符串的比较算法和正整数的比较算法是一样的 + // 而且拼接字符串比较长,会导致 int 类型溢出 + return strs[i]+strs[j] < strs[j]+strs[i] + }) + + return strings.Join(strs, "") +} +``` + +```java +// by labuladong (java) +class Solution { + public String minNumber(int[] nums) { + int n = nums.length; + String[] strs = new String[n]; + for (int i = 0; i < n; i++) { + strs[i] = Integer.toString(nums[i]); + } + Arrays.sort(strs, (s1, s2) -> { + // 看看那种拼接方式得到的数字更小,排前面 + // 不用转成 int 类型,因为字符串的比较算法和正整数的比较算法是一样的 + // 而且拼接字符串比较长,会导致 int 类型溢出 + return (s1 + s2).compareTo(s2 + s1); + }); + + return String.join("", strs); + } +} +``` + +```javascript +// by chatGPT (javascript) +var minNumber = function(nums) { + const n = nums.length; + const strs = new Array(n); + for (let i = 0; i < n; i++) { + strs[i] = nums[i].toString(); + } + strs.sort((s1, s2) => { + // 看看那种拼接方式得到的数字更小,排前面 + // 不用转成 int 类型,因为字符串的比较算法和正整数的比较算法是一样的 + // 而且拼接字符串比较长,会导致 int 类型溢出 + return (s1 + s2).localeCompare(s2 + s1); + }); + + return strs.join(""); +} +``` + +```python +# by chatGPT (python) +class Solution: + def minNumber(self, nums: List[int]) -> str: + n = len(nums) + strs = [str(num) for num in nums] + + def compare(s1: str, s2: str) -> int: + # 看看那种拼接方式得到的数字更小,排前面 + # 不用转成 int 类型,因为字符串的比较算法和正整数的比较算法是一样的 + # 而且拼接字符串比较长,会导致 int 类型溢出 + return -1 if s1 + s2 < s2 + s1 else 1 + + strs.sort(key=functools.cmp_to_key(compare)) + + return ''.join(strs) +``` + +https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int strToInt(string str) { + int n = str.length(); + int i = 0; + // 记录正负号 + int sign = 1; + // 用 long 避免 int 溢出 + long res = 0; + // 跳过前导空格 + while (i < n && str[i] == ' ') { + i++; + } + if (i == n) { + return 0; + } + + // 记录符号位 + if (str[i] == '-') { + sign = -1; + i++; + } else if (str[i] == '+') { + i++; + } + if (i == n) { + return 0; + } + + // 统计数字位 + while (i < n && '0' <= str[i] && str[i] <= '9') { + res = res * 10 + str[i] - '0'; + if (res > INT_MAX) { + break; + } + i++; + } + // 如果溢出,强转成 int 就会和真实值不同 + if ((int) res != res) { + return sign == 1 ? INT_MAX : INT_MIN; + } + return (int) res * sign; + } +}; +``` + +```go +// by chatGPT (go) +func myAtoi(str string) int { + n := len(str) + i := 0 + // 记录正负号 + sign := 1 + // 用 long 避免 int 溢出 + var res int64 = 0 + // 跳过前导空格 + for i < n && str[i] == ' ' { + i++ + } + if i == n { + return 0 + } + + // 记录符号位 + if str[i] == '-' { + sign = -1 + i++ + } else if str[i] == '+' { + i++ + } + if i == n { + return 0 + } + + // 统计数字位 + for i < n && '0' <= str[i] && str[i] <= '9' { + res = res * 10 + int64(str[i]-'0') + if res > math.MaxInt32 { + break + } + i++ + } + // 如果溢出,强转成 int 就会和真实值不同 + if int(res) != res { + if sign == 1 { + return math.MaxInt32 + } else { + return math.MinInt32 + } + } + return int(res) * sign +} +``` + +```java +// by labuladong (java) +class Solution { + public int myAtoi(String str) { + int n = str.length(); + int i = 0; + // 记录正负号 + int sign = 1; + // 用 long 避免 int 溢出 + long res = 0; + // 跳过前导空格 + while (i < n && str.charAt(i) == ' ') { + i++; + } + if (i == n) { + return 0; + } + + // 记录符号位 + if (str.charAt(i) == '-') { + sign = -1; + i++; + } else if (str.charAt(i) == '+') { + i++; + } + if (i == n) { + return 0; + } + + // 统计数字位 + while (i < n && '0' <= str.charAt(i) && str.charAt(i) <= '9') { + res = res * 10 + str.charAt(i) - '0'; + if (res > Integer.MAX_VALUE) { + break; + } + i++; + } + // 如果溢出,强转成 int 就会和真实值不同 + if ((int) res != res) { + return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + return (int) res * sign; + } +} +``` + +```javascript +// by chatGPT (javascript) +var myAtoi = function(str) { + let n = str.length; + let i = 0; + // 记录正负号 + let sign = 1; + // 用 BigInt 避免 int 溢出 + let res = BigInt(0); + // 跳过前导空格 + while (i < n && str[i] === ' ') { + i++; + } + if (i === n) { + return 0; + } + + // 记录符号位 + if (str[i] === '-') { + sign = -1; + i++; + } else if (str[i] === '+') { + i++; + } + if (i === n) { + return 0; + } + + // 统计数字位 + while (i < n && '0' <= str[i] && str[i] <= '9') { + res = res * BigInt(10) + BigInt(str[i].charCodeAt() - '0'.charCodeAt()); + if (res > BigInt(Number.MAX_VALUE)) { + break; + } + i++; + } + // 如果溢出,强转成 int 就会和真实值不同 + if (res > BigInt(Number.MAX_VALUE)) { + return sign === 1 ? Number.MAX_VALUE : Number.MIN_VALUE; + } + return Number(res) * sign; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def myAtoi(self, s: str) -> int: + n = len(s) + i = 0 + # 记录正负号 + sign = 1 + # 用 long 避免 int 溢出 + res = 0 + # 跳过前导空格 + while i < n and s[i] == ' ': + i += 1 + if i == n: + return 0 + + # 记录符号位 + if s[i] == '-': + sign = -1 + i += 1 + elif s[i] == '+': + i += 1 + if i == n: + return 0 + + # 统计数字位 + while i < n and '0' <= s[i] <= '9': + res = res * 10 + ord(s[i]) - ord('0') + if res > pow(2, 31) - 1: + break + i += 1 + # 如果溢出,强转成 int 就会和真实值不同 + if res != int(res): + return sign == 1 and pow(2, 31) - 1 or -pow(2, 31) + return int(res) * sign +``` + +https://leetcode.cn/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/bao-han-minhan-shu-de-zhan-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 原始思路 +class MinStack1 { + // 记录栈中的所有元素 + stack stk; + // 阶段性记录栈中的最小元素 + stack minStk; + +public: + void push(int val) { + stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (minStk.empty() || val <= minStk.top()) { + // 新插入的这个元素就是全栈最小的 + minStk.push(val); + } + else { + // 插入的这个元素比较大 + minStk.push(minStk.top()); + } + } + + void pop() { + stk.pop(); + minStk.pop(); + } + + int top() { + return stk.top(); + } + + int getMin() { + // minStk 栈顶为全栈最小元素 + return minStk.top(); + } +}; +// 优化版 +class MinStack { + // 记录栈中的所有元素 + stack stk; + // 阶段性记录栈中的最小元素 + stack minStk; + +public: + void push(int val) { + stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (minStk.empty() || val <= minStk.top()) { + // 新插入的这个元素就是全栈最小的 + minStk.push(val); + } + } + + void pop() { + // 注意 Java 的语言特性,比较 Integer 相等要用 equals 方法 + if (stk.top() == minStk.top()) { + // 弹出的元素是全栈最小的 + minStk.pop(); + } + stk.pop(); + } + + int top() { + return stk.top(); + } + + int getMin() { + // minStk 栈顶为全栈最小元素 + return minStk.top(); + } +}; +``` + +```go +// by chatGPT (go) +type MinStack1 struct { + stk []int + minStk []int +} + +func (this *MinStack1) Push(val int) { + this.stk = append(this.stk, val) + // 维护 minStk 栈顶为全栈最小元素 + if len(this.minStk) == 0 || val <= this.minStk[len(this.minStk)-1] { + // 新插入的这个元素就是全栈最小的 + this.minStk = append(this.minStk, val) + } else { + // 插入的这个元素比较大 + this.minStk = append(this.minStk, this.minStk[len(this.minStk)-1]) + } +} + +func (this *MinStack1) Pop() { + this.stk = this.stk[:len(this.stk)-1] + this.minStk = this.minStk[:len(this.minStk)-1] +} + +func (this *MinStack1) Top() int { + return this.stk[len(this.stk)-1] +} + +func (this *MinStack1) GetMin() int { + // minStk 栈顶为全栈最小元素 + return this.minStk[len(this.minStk)-1] +} + +type MinStack struct { + stk []int + minStk []int +} + +func (this *MinStack) Push(val int) { + this.stk = append(this.stk, val) + // 维护 minStk 栈顶为全栈最小元素 + if len(this.minStk) == 0 || val <= this.minStk[len(this.minStk)-1] { + // 新插入的这个元素就是全栈最小的 + this.minStk = append(this.minStk, val) + } +} + +func (this *MinStack) Pop() { + // 注意 go 的语言特性,比较 int 相等可以直接使用 == + if this.stk[len(this.stk)-1] == this.minStk[len(this.minStk)-1] { + // 弹出的元素是全栈最小的 + this.minStk = this.minStk[:len(this.minStk)-1] + } + this.stk = this.stk[:len(this.stk)-1] +} + +func (this *MinStack) Top() int { + return this.stk[len(this.stk)-1] +} + +func (this *MinStack) GetMin() int { + // minStk 栈顶为全栈最小元素 + return this.minStk[len(this.minStk)-1] +} +``` + +```java +// by labuladong (java) +// 原始思路 +class MinStack1 { + // 记录栈中的所有元素 + Stack stk = new Stack<>(); + // 阶段性记录栈中的最小元素 + Stack minStk = new Stack<>(); + + public void push(int val) { + stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (minStk.isEmpty() || val <= minStk.peek()) { + // 新插入的这个元素就是全栈最小的 + minStk.push(val); + } else { + // 插入的这个元素比较大 + minStk.push(minStk.peek()); + } + } + + public void pop() { + stk.pop(); + minStk.pop(); + } + + public int top() { + return stk.peek(); + } + + public int getMin() { + // minStk 栈顶为全栈最小元素 + return minStk.peek(); + } +} +// 优化版 +class MinStack { + // 记录栈中的所有元素 + Stack stk = new Stack<>(); + // 阶段性记录栈中的最小元素 + Stack minStk = new Stack<>(); + + public void push(int val) { + stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (minStk.isEmpty() || val <= minStk.peek()) { + // 新插入的这个元素就是全栈最小的 + minStk.push(val); + } + } + + public void pop() { + // 注意 Java 的语言特性,比较 Integer 相等要用 equals 方法 + if (stk.peek().equals(minStk.peek())) { + // 弹出的元素是全栈最小的 + minStk.pop(); + } + stk.pop(); + } + + public int top() { + return stk.peek(); + } + + public int getMin() { + // minStk 栈顶为全栈最小元素 + return minStk.peek(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @constructor + */ +var MinStack1 = function() { + // 记录栈中的所有元素 + this.stk = []; + // 阶段性记录栈中的最小元素 + this.minStk = []; +}; + +/** + * @param {number} val + * @return {void} + */ +MinStack1.prototype.push = function(val) { + this.stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (this.minStk.length === 0 || val <= this.minStk[this.minStk.length - 1]) { + // 新插入的这个元素就是全栈最小的 + this.minStk.push(val); + } else { + // 插入的这个元素比较大 + this.minStk.push(this.minStk[this.minStk.length - 1]); + } +}; + +/** + * @return {void} + */ +MinStack1.prototype.pop = function() { + this.stk.pop(); + this.minStk.pop(); +}; + +/** + * @return {number} + */ +MinStack1.prototype.top = function() { + return this.stk[this.stk.length - 1]; +}; + +/** + * @return {number} + */ +MinStack1.prototype.getMin = function() { + // minStk 栈顶为全栈最小元素 + return this.minStk[this.minStk.length - 1]; +}; + +/** + * @constructor + */ +var MinStack = function() { + // 记录栈中的所有元素 + this.stk = []; + // 阶段性记录栈中的最小元素 + this.minStk = []; +}; + +/** + * @param {number} val + * @return {void} + */ +MinStack.prototype.push = function(val) { + this.stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (this.minStk.length === 0 || val <= this.minStk[this.minStk.length - 1]) { + // 新插入的这个元素就是全栈最小的 + this.minStk.push(val); + } +}; + +/** + * @return {void} + */ +MinStack.prototype.pop = function() { + // 注意 JavaScript 的语言特性,比较 Number 可以直接使用 === + if (this.stk[this.stk.length - 1] === this.minStk[this.minStk.length - 1]) { + // 弹出的元素是全栈最小的 + this.minStk.pop(); + } + this.stk.pop(); +}; + +/** + * @return {number} + */ +MinStack.prototype.top = function() { + return this.stk[this.stk.length - 1]; +}; + +/** + * @return {number} + */ +MinStack.prototype.getMin = function() { + // minStk 栈顶为全栈最小元素 + return this.minStk[this.minStk.length - 1]; +}; +``` + +```python +# by chatGPT (python) +# 原始思路 +class MinStack1: + def __init__(self): + # 记录栈中的所有元素 + self.stk = [] + # 阶段性记录栈中的最小元素 + self.minStk = [] + + def push(self, val: int) -> None: + self.stk.append(val) + # 维护 minStk 栈顶为全栈最小元素 + if not self.minStk or val <= self.minStk[-1]: + # 新插入的这个元素就是全栈最小的 + self.minStk.append(val) + else: + # 插入的这个元素比较大 + self.minStk.append(self.minStk[-1]) + + def pop(self) -> None: + self.stk.pop() + self.minStk.pop() + + def top(self) -> int: + return self.stk[-1] + + def getMin(self) -> int: + # minStk 栈顶为全栈最小元素 + return self.minStk[-1] + + +# 优化版 +class MinStack: + def __init__(self): + # 记录栈中的所有元素 + self.stk = [] + # 阶段性记录栈中的最小元素 + self.minStk = [] + + def push(self, val: int) -> None: + self.stk.append(val) + # 维护 minStk 栈顶为全栈最小元素 + if not self.minStk or val <= self.minStk[-1]: + # 新插入的这个元素就是全栈最小的 + self.minStk.append(val) + + def pop(self) -> None: + # 注意 Python 的语言特性,比较 Integer 相等不需要调用 equals 方法 + if self.stk[-1] == self.minStk[-1]: + # 弹出的元素是全栈最小的 + self.minStk.pop() + self.stk.pop() + + def top(self) -> int: + return self.stk[-1] + + def getMin(self) -> int: + # minStk 栈顶为全栈最小元素 + return self.minStk[-1] +``` + +https://leetcode.cn/problems/bao-han-minhan-shu-de-zhan-lcof 的多语言解法👆 + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxProfit(vector& prices) { + int n = prices.size(); + vector> dp(n, vector(2)); + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = max(dp[i - 1][1], -prices[i]); + } + return dp[n - 1][0]; + } +}; +``` + +```go +// by chatGPT (go) +func maxProfit(prices []int) int { + n := len(prices) + dp := make([][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([]int, 2) + if i - 1 == -1 { + // base case + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + } + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + dp[i][1] = max(dp[i-1][1], -prices[i]) + } + return dp[n-1][0] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxProfit(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + } + return dp[n - 1][0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} prices + * @return {number} + */ +var maxProfit = function(prices) { + const n = prices.length; + const dp = new Array(n).map(() => new Array(2)); + for (let i = 0; i < n; i++) { + if (i - 1 === -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + } + return dp[n - 1][0]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxProfit(self, prices: List[int]) -> int: + n = len(prices) + dp = [[0] * 2 for _ in range(n)] + for i in range(n): + if i - 1 == -1: + # base case + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) + dp[i][1] = max(dp[i - 1][1], -prices[i]) + return dp[n - 1][0] +``` + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock 的多语言解法👆 + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxProfit(vector& prices) { + int n = prices.size(); + vector> dp(n, vector(2)); + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + } + return dp[n - 1][0]; + } +}; +``` + +```go +// by chatGPT (go) +func maxProfit(prices []int) int { + n := len(prices) + dp := make([][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([]int, 2) + if i-1 == -1 { + // base case + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + } + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]) + } + return dp[n-1][0] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxProfit(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + } + return dp[n - 1][0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxProfit = function(prices) { + const n = prices.length; + const dp = new Array(n).fill(0).map(() => new Array(2).fill(0)); + for (let i = 0; i < n; i++) { + if (i - 1 === -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + } + return dp[n - 1][0]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxProfit(self, prices: List[int]) -> int: + n = len(prices) + dp = [[0] * 2 for _ in range(n)] + for i in range(n): + if i - 1 == -1: + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]) + return dp[n - 1][0] +``` + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii 的多语言解法👆 + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxProfit(vector& prices) { + int max_k = 2, n = prices.size(); + int dp[n][max_k + 1][2]; + for (int i = 0; i < n; i++) { + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); + dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); + } + } + // 穷举了 n × max_k × 2 个状态,正确。 + return dp[n - 1][max_k][0]; + } +}; +``` + +```go +// by chatGPT (go) +func maxProfit(prices []int) int { + max_k, n := 2, len(prices) + dp := make([][][]int, n) + for i := range dp { + dp[i] = make([][]int, max_k+1) + for j := range dp[i] { + dp[i][j] = make([]int, 2) + } + } + for i := 0; i < n; i++ { + for k := max_k; k >= 1; k-- { + if i-1 == -1 { + // 处理 base case + dp[i][k][0] = 0 + dp[i][k][1] = -prices[i] + continue + } + dp[i][k][0] = func() int { + if dp[i-1][k][0] > dp[i-1][k][1]+prices[i] { + return dp[i-1][k][0] + } + return dp[i-1][k][1] + prices[i] + }() + dp[i][k][1] = func() int { + if dp[i-1][k][1] > dp[i-1][k-1][0]-prices[i] { + return dp[i-1][k][1] + } + return dp[i-1][k-1][0] - prices[i] + }() + } + } + // 穷举了 n × max_k × 2 个状态,正确。 + return dp[n-1][max_k][0] +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxProfit(int[] prices) { + int max_k = 2, n = prices.length; + int[][][] dp = new int[n][max_k + 1][2]; + for (int i = 0; i < n; i++) { + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); + } + } + // 穷举了 n × max_k × 2 个状态,正确。 + return dp[n - 1][max_k][0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxProfit = function(prices) { + let max_k = 2, n = prices.length; + let dp = Array(n).fill().map(() => Array(max_k + 1).fill().map(() => Array(2).fill(0))); + for (let i = 0; i < n; i++) { + for (let k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); + } + } + // 穷举了 n × max_k × 2 个状态,正确。 + return dp[n - 1][max_k][0]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxProfit(self, prices: List[int]) -> int: + max_k = 2 + n = len(prices) + dp = [[[0 for _ in range(2)] for _ in range(max_k + 1)] for _ in range(n)] + for i in range(n): + for k in range(max_k, 0, -1): + if i - 1 == -1: + # 处理 base case + dp[i][k][0] = 0 + dp[i][k][1] = -prices[i] + continue + dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) + dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) + # 穷举了 n × max_k × 2 个状态,正确。 + return dp[n - 1][max_k][0] +``` + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii 的多语言解法👆 + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxProfit(int max_k, vector& prices) { + int n = prices.size(); + if (n <= 0) { + return 0; + } + if (max_k > n / 2) { + // 交易次数 k 没有限制的情况 + return maxProfit_k_inf(prices); + } + + // base case: + // dp[-1][...][0] = dp[...][0][0] = 0 + // dp[-1][...][1] = dp[...][0][1] = -infinity + vector>> dp(n, vector>(max_k + 1, vector(2))); + // k = 0 时的 base case + for (int i = 0; i < n; i++) { + dp[i][0][1] = INT_MIN; + dp[i][0][0] = 0; + } + + for (int i = 0; i < n; i++) { + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 i = -1 时的 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + // 状态转移方程 + dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i]); + dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i]); + } + } + return dp[n - 1][max_k][0]; + } + +private: + // 第 122 题,k 无限的解法 + int maxProfit_k_inf(vector& prices) { + int n = prices.size(); + vector> dp(n, vector(2)); + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + } + return dp[n - 1][0]; + } +}; +``` + +```go +// by chatGPT (go) +func maxProfit(max_k int, prices []int) int { + n := len(prices) + if n <= 0 { + return 0 + } + if max_k > n/2 { + // 交易次数 k 没有限制的情况 + return maxProfitKInf(prices) + } + + // base case: + // dp[-1][...][0] = dp[...][0][0] = 0 + // dp[-1][...][1] = dp[...][0][1] = -infinity + dp := make([][][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([][]int, max_k+1) + for k := 0; k <= max_k; k++ { + dp[i][k] = make([]int, 2) + } + } + + // k = 0 时的 base case + for i := 0; i < n; i++ { + dp[i][0][1] = -1 << 31 + dp[i][0][0] = 0 + } + + for i := 0; i < n; i++ { + for k := max_k; k >= 1; k-- { + if i-1 == -1 { + // 处理 i = -1 时的 base case + dp[i][k][0] = 0 + dp[i][k][1] = -prices[i] + continue + } + // 状态转移方程 + dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]) + dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]) + } + } + return dp[n-1][max_k][0] +} + +// 第 122 题,k 无限的解法 +func maxProfitKInf(prices []int) int { + n := len(prices) + dp := make([][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([]int, 2) + } + + for i := 0; i < n; i++ { + if i-1 == -1 { + // base case + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + } + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]) + } + return dp[n-1][0] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxProfit(int max_k, int[] prices) { + int n = prices.length; + if (n <= 0) { + return 0; + } + if (max_k > n / 2) { + // 交易次数 k 没有限制的情况 + return maxProfit_k_inf(prices); + } + + // base case: + // dp[-1][...][0] = dp[...][0][0] = 0 + // dp[-1][...][1] = dp[...][0][1] = -infinity + int[][][] dp = new int[n][max_k + 1][2]; + // k = 0 时的 base case + for (int i = 0; i < n; i++) { + dp[i][0][1] = Integer.MIN_VALUE; + dp[i][0][0] = 0; + } + + for (int i = 0; i < n; i++) + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 i = -1 时的 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + // 状态转移方程 + dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i]); + } + return dp[n - 1][max_k][0]; + } + + // 第 122 题,k 无限的解法 + private int maxProfit_k_inf(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + } + return dp[n - 1][0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxProfit = function(max_k, prices) { + var n = prices.length; + if (n <= 0) { + return 0; + } + if (max_k > Math.floor(n / 2)) { + // 交易次数 k 没有限制的情况 + return maxProfit_k_inf(prices); + } + + // base case: + // dp[-1][...][0] = dp[...][0][0] = 0 + // dp[-1][...][1] = dp[...][0][1] = -infinity + var dp = new Array(n).fill(0).map(()=>new Array(max_k + 1).fill(0).map(()=>new Array(2).fill(0))); + // k = 0 时的 base case + for (var i = 0; i < n; i++) { + dp[i][0][1] = Number.NEGATIVE_INFINITY; + dp[i][0][0] = 0; + } + + for (var i = 0; i < n; i++) + for (var k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 i = -1 时的 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + // 状态转移方程 + dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i]); + } + return dp[n - 1][max_k][0]; +} + +// 第 122 题,k 无限的解法 +var maxProfit_k_inf = function(prices) { + var n = prices.length; + var dp = new Array(n).fill(0).map(()=>new Array(2).fill(0)); + for (var i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + } + return dp[n - 1][0]; +} +``` + +```python +# by chatGPT (python) +class Solution: + def maxProfit(self, max_k: int, prices: List[int]) -> int: + n = len(prices) + if n <= 0: + return 0 + if max_k > n // 2: + # 交易次数 k 没有限制的情况 + return self.maxProfit_k_inf(prices) + + # base case: + # dp[-1][...][0] = dp[...][0][0] = 0 + # dp[-1][...][1] = dp[...][0][1] = -infinity + dp = [[[0] * 2 for _ in range(max_k + 1)] for _ in range(n)] + # k = 0 时的 base case + for i in range(n): + dp[i][0][1] = float('-inf') + dp[i][0][0] = 0 + + for i in range(n): + for k in range(max_k, 0, -1): + if i - 1 == -1: + # 处理 i = -1 时的 base case + dp[i][k][0] = 0 + dp[i][k][1] = -prices[i] + continue + # 状态转移方程 + dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i]) + dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i]) + return dp[n - 1][max_k][0] + + # 第 122 题,k 无限的解法 + def maxProfit_k_inf(self, prices: List[int]) -> int: + n = len(prices) + dp = [[0] * 2 for _ in range(n)] + for i in range(n): + if i - 1 == -1: + # base case + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]) + return dp[n - 1][0] +``` + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv 的多语言解法👆 + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxProfit(vector& prices) { + int n = prices.size(); + vector> dp(n, vector(2)); + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case 1 + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + if (i - 2 == -1) { + // base case 2 + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + // i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = max(dp[i - 1][1], -prices[i]); + // dp[i][1] + // = max(dp[i-1][1], dp[-1][0] - prices[i]) + // = max(dp[i-1][1], 0 - prices[i]) + // = max(dp[i-1][1], -prices[i]) + continue; + } + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i]); + } + return dp[n - 1][0]; + } +}; +``` + +```go +// by chatGPT (go) +func maxProfit(prices []int) int { + n := len(prices) + dp := make([][2]int, n) + for i := 0; i < n; i++ { + if i-1 == -1 { + // base case 1 + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + } + if i-2 == -1 { + // base case 2 + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + // i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = max(dp[i-1][1], -prices[i]) + // dp[i][1] + // = max(dp[i-1][1], dp[-1][0] - prices[i]) + // = max(dp[i-1][1], 0 - prices[i]) + // = max(dp[i-1][1], -prices[i]) + continue + } + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-2][0]-prices[i]) + } + return dp[n-1][0] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxProfit(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case 1 + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + if (i - 2 == -1) { + // base case 2 + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + // i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + // dp[i][1] + // = max(dp[i-1][1], dp[-1][0] - prices[i]) + // = max(dp[i-1][1], 0 - prices[i]) + // = max(dp[i-1][1], -prices[i]) + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]); + } + return dp[n - 1][0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} prices + * @return {number} + */ +var maxProfit = function(prices) { + let n = prices.length; + let dp = Array.from(Array(n), () => new Array(2).fill(0)); + for (let i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case 1 + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + if (i - 2 == -1) { + // base case 2 + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + // i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + // dp[i][1] + // = max(dp[i-1][1], dp[-1][0] - prices[i]) + // = max(dp[i-1][1], 0 - prices[i]) + // = max(dp[i-1][1], -prices[i]) + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]); + } + return dp[n - 1][0]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxProfit(self, prices: List[int]) -> int: + n = len(prices) + dp = [[0]*2 for _ in range(n)] + for i in range(n): + if i - 1 == -1: + # base case 1 + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + if i - 2 == -1: + # base case 2 + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) + # i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = max(dp[i - 1][1], -prices[i]) + # dp[i][1] + # = max(dp[i-1][1], dp[-1][0] - prices[i]) + # = max(dp[i-1][1], 0 - prices[i]) + # = max(dp[i-1][1], -prices[i]) + continue + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) + dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i]) + return dp[n - 1][0] +``` + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown 的多语言解法👆 + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxProfit(vector& prices, int fee) { + int n = prices.size(); + vector> dp(n, vector(2)); + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i] - fee; + // dp[i][1] + // = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + // = max(dp[-1][1], dp[-1][0] - prices[i] - fee) + // = max(-inf, 0 - prices[i] - fee) + // = -prices[i] - fee + continue; + } + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee); + } + return dp[n - 1][0]; + } +}; +``` + +```go +// by chatGPT (go) +func maxProfit(prices []int, fee int) int { + n := len(prices) + dp := make([][2]int, n) + for i := 0; i < n; i++ { + if i-1 == -1 { + // base case + dp[i][0] = 0 + dp[i][1] = -prices[i] - fee + // dp[i][1] + // = max(dp[i-1][1], dp[i-1][0]-prices[i]-fee) + // = max(dp[-1][1], dp[-1][0]-prices[i]-fee) + // = max(-inf, 0-prices[i]-fee) + // = -prices[i]-fee + continue + } + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]-fee) + } + return dp[n-1][0] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxProfit(int[] prices, int fee) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i] - fee; + // dp[i][1] + // = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + // = max(dp[-1][1], dp[-1][0] - prices[i] - fee) + // = max(-inf, 0 - prices[i] - fee) + // = -prices[i] - fee + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee); + } + return dp[n - 1][0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxProfit = function(prices, fee) { + const n = prices.length; + const dp = new Array(n).map(() => new Array(2).fill(0)); + for (let i = 0; i < n; i++) { + if (i - 1 === -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i] - fee; + // dp[i][1] + // = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + // = max(dp[-1][1], dp[-1][0] - prices[i] - fee) + // = max(-inf, 0 - prices[i] - fee) + // = -prices[i] - fee + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee); + } + return dp[n - 1][0]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxProfit(self, prices: List[int], fee: int) -> int: + n = len(prices) + dp = [[0]*2 for _ in range(n)] + for i in range(n): + if i - 1 == -1: + # base case + dp[i][0] = 0 + dp[i][1] = -prices[i] - fee + # dp[i][1] + # = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + # = max(dp[-1][1], dp[-1][0] - prices[i] - fee) + # = max(-inf, 0 - prices[i] - fee) + # = -prices[i] - fee + continue + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + return dp[n - 1][0] +``` + +https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee 的多语言解法👆 + +https://leetcode.cn/problems/binary-search 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int search(vector& nums, int target) { + int left = 0; + int right = nums.size() - 1; // 注意 + + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) + return mid; + else if (nums[mid] < target) + left = mid + 1; // 注意 + else if (nums[mid] > target) + right = mid - 1; // 注意 + } + return -1; + } +}; +``` + +```go +// by chatGPT (go) +func search(nums []int, target int) int { + left := 0 + right := len(nums) - 1 // 注意 + + for left <= right { + mid := left + (right - left) / 2 + if nums[mid] == target { + return mid + } else if nums[mid] < target { + left = mid + 1 // 注意 + } else if nums[mid] > target { + right = mid - 1 // 注意 + } + } + return -1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int search(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; // 注意 + + while(left <= right) { + int mid = left + (right - left) / 2; + if(nums[mid] == target) + return mid; + else if (nums[mid] < target) + left = mid + 1; // 注意 + else if (nums[mid] > target) + right = mid - 1; // 注意 + } + return -1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var search = function(nums, target) { + var left = 0; + var right = nums.length - 1; // 注意 + + while (left <= right) { + var mid = left + Math.floor((right - left) / 2); + if (nums[mid] == target) + return mid; + else if (nums[mid] < target) + left = mid + 1; // 注意 + else if (nums[mid] > target) + right = mid - 1; // 注意 + } + return -1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def search(self, nums: List[int], target: int) -> int: + left = 0 + right = len(nums) - 1 # 注意 + + while left <= right: + mid = left + (right - left) // 2 + if nums[mid] == target: + return mid + elif nums[mid] < target: + left = mid + 1 # 注意 + elif nums[mid] > target: + right = mid - 1 # 注意 + + return -1 +``` + +https://leetcode.cn/problems/binary-search 的多语言解法👆 + +https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + TreeNode* bstToGst(TreeNode* root) { + traverse(root); + return root; + } + + // 记录累加和 + int sum = 0; + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + traverse(root->right); + // 维护累加和 + sum += root->val; + // 将 BST 转化成累加树 + root->val = sum; + traverse(root->left); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func bstToGst(root *TreeNode) *TreeNode { + var sum int + // 定义中序遍历函数,内部维护sum变量 + var traverse func(node *TreeNode) + traverse = func(node *TreeNode) { + if node == nil { + return + } + traverse(node.Right) + // 维护累加和 + sum += node.Val + // 将BST转化为累加树 + node.Val = sum + traverse(node.Left) + } + traverse(root) + return root +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode bstToGst(TreeNode root) { + traverse(root); + return root; + } + + // 记录累加和 + int sum = 0; + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.right); + // 维护累加和 + sum += root.val; + // 将 BST 转化成累加树 + root.val = sum; + traverse(root.left); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {TreeNode} + */ +var bstToGst = function(root) { + let sum = 0; + + const traverse = function(root) { + if (root == null) { + return; + } + traverse(root.right); + // 维护累加和 + sum += root.val; + // 将 BST 转化成累加树 + root.val = sum; + traverse(root.left); + } + + traverse(root); + return root; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def bstToGst(self, root: TreeNode) -> TreeNode: + # 记录累加和 + sum = 0 + + # 中序遍历BST,递归地对BST节点的值进行累加和操作 + def traverse(root: TreeNode) -> None: + nonlocal sum + if not root: + return + traverse(root.right) + # 维护累加和 + sum += root.val + # 将 BST 转化成累加树 + root.val = sum + traverse(root.left) + + traverse(root) + return root +``` + +https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree 的多语言解法👆 + +https://leetcode.cn/problems/binary-tree-inorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的中序遍历结果 + vector inorderTraversal(TreeNode* root) { + vector res; + if (root == NULL) { + return res; + } + vector left = inorderTraversal(root->left); + res.insert(res.end(), left.begin(), left.end()); + res.push_back(root->val); + vector right = inorderTraversal(root->right); + res.insert(res.end(), right.begin(), right.end()); + return res; + } + + /* 回溯算法思路 */ + vector res; + + // 返回前序遍历结果 + vector inorderTraversal2(TreeNode* root) { + traverse(root); + return res; + } + + // 二叉树遍历函数 + void traverse(TreeNode* root) { + if (root == NULL) { + return; + } + traverse(root->left); + // 中序遍历位置 + res.push_back(root->val); + traverse(root->right); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +// 动态规划思路 +// 定义:输入一个节点,返回以该节点为根的二叉树的中序遍历结果 +func inorderTraversal(root *TreeNode) []int { + res := []int{} + if root == nil { + return res + } + res = append(res, inorderTraversal(root.Left)...) + res = append(res, root.Val) + res = append(res, inorderTraversal(root.Right)...) + return res +} + +// 回溯算法思路 +func inorderTraversal2(root *TreeNode) []int { + res := []int{} + // 二叉树遍历函数 + var traverse func(node *TreeNode) + traverse = func(node *TreeNode) { + if node == nil { + return + } + traverse(node.Left) + // 中序遍历位置 + res = append(res, node.Val) + traverse(node.Right) + } + + traverse(root) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的中序遍历结果 + public List inorderTraversal(TreeNode root) { + LinkedList res = new LinkedList<>(); + if (root == null) { + return res; + } + res.addAll(inorderTraversal(root.left)); + res.add(root.val); + res.addAll(inorderTraversal(root.right)); + return res; + } + + /* 回溯算法思路 */ + LinkedList res = new LinkedList<>(); + + // 返回前序遍历结果 + public List inorderTraversal2(TreeNode root) { + traverse(root); + return res; + } + + // 二叉树遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.left); + // 中序遍历位置 + res.add(root.val); + traverse(root.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +var inorderTraversal = function(root) { + let res = new LinkedList(); + if (root === null) { + return res; + } + res.addAll(inorderTraversal(root.left)); + res.add(root.val); + res.addAll(inorderTraversal(root.right)); + return res; +}; + +var inorderTraversal2 = function(root) { + let res = new LinkedList(); + if (root === null) { + return res; + } + + traverse(root); + return res; +}; + +const traverse = function(root, res) { + if (root === null) { + return; + } + traverse(root.left, res); + // 中序遍历位置 + res.add(root.val); + traverse(root.right, res); +} +``` + +```python +# by chatGPT (python) +class Solution: + def inorderTraversal(self, root: TreeNode) -> List[int]: + """动态规划思路 + 定义:输入一个节点,返回以该节点为根的二叉树的中序遍历结果 + """ + res = [] + if not root: + return res + res += self.inorderTraversal(root.left) + res.append(root.val) + res += self.inorderTraversal(root.right) + return res + + def inorderTraversal2(self, root: TreeNode) -> List[int]: + """回溯算法思路""" + self.res = [] + self.traverse(root) + return self.res + + def traverse(self, root: TreeNode): + """二叉树遍历函数""" + if not root: + return + self.traverse(root.left) + # 中序遍历位置 + self.res.append(root.val) + self.traverse(root.right) +``` + +https://leetcode.cn/problems/binary-tree-inorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/binary-tree-level-order-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> levelOrder(TreeNode* root) { + vector> res; + if (root == nullptr) { + return res; + } + + queue q; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (!q.empty()) { + int sz = q.size(); + // 记录这一层的节点值 + vector level; + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode* cur = q.front(); + q.pop(); + level.push_back(cur->val); + if (cur->left != nullptr) + q.push(cur->left); + if (cur->right != nullptr) + q.push(cur->right); + } + res.push_back(level); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func levelOrder(root *TreeNode) [][]int { + res := [][]int{} + if root == nil { + return res + } + + q := []*TreeNode{root} + // while 循环控制从上向下一层层遍历 + for len(q) > 0 { + sz := len(q) + // 记录这一层的节点值 + level := []int{} + // for 循环控制每一层从左向右遍历 + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + level = append(level, cur.Val) + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + res = append(res, level) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> levelOrder(TreeNode root) { + List> res = new LinkedList<>(); + if (root == null) { + return res; + } + + Queue q = new LinkedList<>(); + q.offer(root); + // while 循环控制从上向下一层层遍历 + while (!q.isEmpty()) { + int sz = q.size(); + // 记录这一层的节点值 + List level = new LinkedList<>(); + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + level.add(cur.val); + if (cur.left != null) + q.offer(cur.left); + if (cur.right != null) + q.offer(cur.right); + } + res.add(level); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var levelOrder = function(root) { + let res = []; + if (root == null) { + return res; + } + + let q = []; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (q.length > 0) { + let sz = q.length; + // 记录这一层的节点值 + let level = []; + // for 循环控制每一层从左向右遍历 + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + level.push(cur.val); + if (cur.left != null) + q.push(cur.left); + if (cur.right != null) + q.push(cur.right); + } + res.push(level); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def levelOrder(self, root: TreeNode) -> List[List[int]]: + res = [] + if not root: + return res + + q = [] + q.append(root) + # while 循环控制从上向下一层层遍历 + while q: + sz = len(q) + # 记录这一层的节点值 + level = [] + # for 循环控制每一层从左向右遍历 + for i in range(sz): + cur = q.pop(0) + level.append(cur.val) + if cur.left: + q.append(cur.left) + if cur.right: + q.append(cur.right) + res.append(level) + return res +``` + +https://leetcode.cn/problems/binary-tree-level-order-traversal 的多语言解法👆 + +https://leetcode.cn/problems/binary-tree-level-order-traversal-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> levelOrderBottom(TreeNode* root) { + vector> res; + if (root == NULL) { + return res; + } + + queue q; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (!q.empty()) { + int sz = q.size(); + // 记录这一层的节点值 + vector level; + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode* cur = q.front(); + q.pop(); + level.push_back(cur->val); + if (cur->left != NULL) + q.push(cur->left); + if (cur->right != NULL) + q.push(cur->right); + } + // 把每一层添加到头部,就是自底向上的层序遍历。 + res.insert(res.begin(), level); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func levelOrderBottom(root *TreeNode) [][]int { + res := make([][]int, 0) + if root == nil { + return res + } + + q := make([]*TreeNode, 0) + q = append(q, root) + // while 循环控制从上向下一层层遍历 + for len(q) > 0 { + sz := len(q) + // 记录这一层的节点值 + level := make([]int, 0) + // for 循环控制每一层从左向右遍历 + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + level = append(level, cur.Val) + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + // 把每一层添加到头部,就是自底向上的层序遍历。 + res = append([][]int{level}, res...) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> levelOrderBottom(TreeNode root) { + LinkedList> res = new LinkedList<>(); + if (root == null) { + return res; + } + + Queue q = new LinkedList<>(); + q.offer(root); + // while 循环控制从上向下一层层遍历 + while (!q.isEmpty()) { + int sz = q.size(); + // 记录这一层的节点值 + List level = new LinkedList<>(); + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + level.add(cur.val); + if (cur.left != null) + q.offer(cur.left); + if (cur.right != null) + q.offer(cur.right); + } + // 把每一层添加到头部,就是自底向上的层序遍历。 + res.addFirst(level); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var levelOrderBottom = function(root) { + let res = []; + if (root == null) { + return res; + } + + let q = []; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (q.length > 0) { + let sz = q.length; + // 记录这一层的节点值 + let level = []; + // for 循环控制每一层从左向右遍历 + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + level.push(cur.val); + if (cur.left != null) + q.push(cur.left); + if (cur.right != null) + q.push(cur.right); + } + // 把每一层添加到头部,就是自底向上的层序遍历。 + res.unshift(level); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def levelOrderBottom(self, root: TreeNode) -> List[List[int]]: + res = [] + if not root: + return res + + q = collections.deque([root]) + # while 循环控制从上向下一层层遍历 + while q: + sz = len(q) + # 记录这一层的节点值 + level = [] + # for 循环控制每一层从左向右遍历 + for i in range(sz): + cur = q.popleft() + level.append(cur.val) + if cur.left: + q.append(cur.left) + if cur.right: + q.append(cur.right) + # 把每一层添加到头部,就是自底向上的层序遍历。 + res.insert(0, level) + return res +``` + +https://leetcode.cn/problems/binary-tree-level-order-traversal-ii 的多语言解法👆 + +https://leetcode.cn/problems/binary-tree-postorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的后序遍历结果 + vector postorderTraversal(TreeNode* root) { + vector res; + if (root == nullptr) { + return res; + } + // 后序遍历结果特点:先是左子树,接着是右子树,最后是根节点的值 + auto left = postorderTraversal(root->left); + auto right = postorderTraversal(root->right); + res.insert(res.end(), left.begin(), left.end()); + res.insert(res.end(), right.begin(), right.end()); + res.push_back(root->val); + return res; + } + + /* 回溯算法思路 */ + vector res; + + // 返回后序遍历结果 + vector postorderTraversal2(TreeNode* root) { + traverse(root); + return res; + } + + // 二叉树遍历函数 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + traverse(root->left); + traverse(root->right); + // 后序遍历位置 + res.push_back(root->val); + } +}; +``` + +```go +// by chatGPT (go) +// 动态规划思路 +// 定义:输入一个节点,返回以该节点为根的二叉树的后序遍历结果 +func postorderTraversal(root *TreeNode) []int { + res := []int{} + if root == nil { + return res + } + // 后序遍历结果特点:先是左子树,接着是右子树,最后是根节点的值 + res = append(res, postorderTraversal(root.Left)...) + res = append(res, postorderTraversal(root.Right)...) + res = append(res, root.Val) + return res +} + +// 回溯算法思路 +func postorderTraversal2(root *TreeNode) []int { + res := []int{} + traverse(root, &res) + return res +} + +// 二叉树遍历函数 +func traverse(root *TreeNode, res *[]int) { + if root == nil { + return + } + traverse(root.Left, res) + traverse(root.Right, res) + // 后序遍历位置 + *res = append(*res, root.Val) +} +``` + +```java +// by labuladong (java) +class Solution { + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的后序遍历结果 + public List postorderTraversal(TreeNode root) { + LinkedList res = new LinkedList<>(); + if (root == null) { + return res; + } + // 后序遍历结果特点:先是左子树,接着是右子树,最后是根节点的值 + res.addAll(postorderTraversal(root.left)); + res.addAll(postorderTraversal(root.right)); + res.add(root.val); + return res; + } + + /* 回溯算法思路 */ + LinkedList res = new LinkedList<>(); + + // 返回后序遍历结果 + public List postorderTraversal2(TreeNode root) { + traverse(root); + return res; + } + + // 二叉树遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.left); + traverse(root.right); + // 后序遍历位置 + res.add(root.val); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {number[]} + */ +var postorderTraversal = function(root) { + const res = []; + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的后序遍历结果 + if (root == null) { + return res; + } + // 后序遍历结果特点:先是左子树,接着是右子树,最后是根节点的值 + res.push(...postorderTraversal(root.left)); + res.push(...postorderTraversal(root.right)); + res.push(root.val); + + /* 回溯算法思路 */ + /*LinkedList res = new LinkedList<>();*/ + + // 返回后序遍历结果 + // public List postorderTraversal2(TreeNode root) { + // traverse(root); + // return res; + // } + + // 二叉树遍历函数 + // void traverse(TreeNode root) { + // if (root == null) { + // return; + // } + // traverse(root.left); + // traverse(root.right); + // // 后序遍历位置 + // res.add(root.val); + // } + + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def postorderTraversal(self, root: TreeNode) -> List[int]: + res = [] + if not root: + return res + # 后序遍历结果特点:先是左子树,接着是右子树,最后是根节点的值 + res += self.postorderTraversal(root.left) + res += self.postorderTraversal(root.right) + res.append(root.val) + return res + + res = [] + + def postorderTraversal2(self, root: TreeNode) -> List[int]: + self.traverse(root) + return self.res + + def traverse(self, root: TreeNode): + if not root: + return + self.traverse(root.left) + self.traverse(root.right) + # 后序遍历位置 + self.res.append(root.val) +``` + +https://leetcode.cn/problems/binary-tree-postorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/binary-tree-preorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的前序遍历结果 + vector preorderTraversal(TreeNode* root) { + vector res; + if (!root) { + return res; + } + // 前序遍历结果特点:第一个是根节点的值,接着是左子树,最后是右子树 + res.push_back(root->val); + vector left = preorderTraversal(root->left); + res.insert(res.end(), left.begin(), left.end()); + vector right = preorderTraversal(root->right); + res.insert(res.end(), right.begin(), right.end()); + return res; + } + + /* 回溯算法思路 */ + vector res; + + // 二叉树遍历函数 + void traverse(TreeNode* root) { + if (!root) { + return; + } + // 前序遍历位置 + res.push_back(root->val); + traverse(root->left); + traverse(root->right); + } + +public: + // 返回前序遍历结果 + vector preorderTraversal2(TreeNode* root) { + traverse(root); + return res; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +func preorderTraversal(root *TreeNode) []int { + var res []int + if root == nil { + return res + } + + // 动态规划思路 + // 前序遍历结果特点:第一个是根节点的值,接着是左子树,最后是右子树 + res = append(res, root.Val) + res = append(res, preorderTraversal(root.Left)...) + res = append(res, preorderTraversal(root.Right)...) + + // 回溯算法思路 + // traverse(root, &res) + + return res +} + +// 回溯算法 +// func traverse(root *TreeNode, res *[]int) { +// if root == nil { +// return +// } +// *res = append(*res, root.Val) +// traverse(root.Left, res) +// traverse(root.Right, res) +// } +``` + +```java +// by labuladong (java) +class Solution { + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的前序遍历结果 + public List preorderTraversal(TreeNode root) { + LinkedList res = new LinkedList<>(); + if (root == null) { + return res; + } + // 前序遍历结果特点:第一个是根节点的值,接着是左子树,最后是右子树 + res.add(root.val); + res.addAll(preorderTraversal(root.left)); + res.addAll(preorderTraversal(root.right)); + return res; + } + + /* 回溯算法思路 */ + LinkedList res = new LinkedList<>(); + + // 返回前序遍历结果 + public List preorderTraversal2(TreeNode root) { + traverse(root); + return res; + } + + // 二叉树遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + // 前序遍历位置 + res.add(root.val); + traverse(root.left); + traverse(root.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +var Solution = function() { + + /* 动态规划思路 */ + // 定义:输入一个节点,返回以该节点为根的二叉树的前序遍历结果 + this.preorderTraversal = function(root) { + let res = []; + if (root == null) { + return res; + } + // 前序遍历结果特点:第一个是根节点的值,接着是左子树,最后是右子树 + res.push(root.val); + res = res.concat(this.preorderTraversal(root.left)); + res = res.concat(this.preorderTraversal(root.right)); + return res; + }; + + /* 回溯算法思路 */ + let res = []; + + // 返回前序遍历结果 + this.preorderTraversal2 = function(root) { + traverse(root); + return res; + }; + + // 二叉树遍历函数 + function traverse(root) { + if (root == null) { + return; + } + // 前序遍历位置 + res.push(root.val); + traverse(root.left); + traverse(root.right); + } +}; +``` + +```python +# by chatGPT (python) +# Definition for a binary tree node. +# class TreeNode: +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right + +class Solution: + # 动态规划思路 + # 定义:输入一个节点,返回以该节点为根的二叉树的前序遍历结果 + def preorderTraversal(self, root: TreeNode) -> List[int]: + res = [] + if not root: + return res + # 前序遍历结果特点:第一个是根节点的值,接着是左子树,最后是右子树 + res.append(root.val) + res.extend(self.preorderTraversal(root.left)) + res.extend(self.preorderTraversal(root.right)) + return res + + # 回溯算法思路 + def __init__(self): + self.res = [] + + # 返回前序遍历结果 + def preorderTraversal2(self, root: TreeNode) -> List[int]: + self.traverse(root) + return self.res + + # 二叉树遍历函数 + def traverse(self, root: TreeNode) -> None: + if not root: + return + # 前序遍历位置 + self.res.append(root.val) + self.traverse(root.left) + self.traverse(root.right) +``` + +https://leetcode.cn/problems/binary-tree-preorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> zigzagLevelOrder(TreeNode* root) { + vector> res; + if (root == nullptr) { + return res; + } + + queue q; + q.push(root); + // 为 true 时向右,false 时向左 + bool flag = true; + + // while 循环控制从上向下一层层遍历 + while (!q.empty()) { + int sz = q.size(); + // 记录这一层的节点值 + list level; + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode* cur = q.front(); + q.pop(); + // 实现 z 字形遍历 + if (flag) { + level.push_back(cur->val); + } else { + level.push_front(cur->val); + } + if (cur->left != nullptr) + q.push(cur->left); + if (cur->right != nullptr) + q.push(cur->right); + } + // 切换方向 + flag = !flag; + res.emplace_back(level.begin(), level.end()); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func zigzagLevelOrder(root *TreeNode) [][]int { + res := make([][]int, 0) + if root == nil { + return res + } + + q := make([]*TreeNode, 0) + q = append(q, root) + // 为 true 时向右,false 时向左 + flag := true + + // while 循环控制从上向下一层层遍历 + for len(q) > 0 { + sz := len(q) + // 记录这一层的节点值 + level := make([]int, 0, sz) + // for 循环控制每一层从左向右遍历 + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + // 实现 z 字形遍历 + if flag { + level = append(level, cur.Val) + } else { + level = append([]int{cur.Val}, level...) + } + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + // 切换方向 + flag = !flag + res = append(res, level) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> zigzagLevelOrder(TreeNode root) { + List> res = new LinkedList<>(); + if (root == null) { + return res; + } + + Queue q = new LinkedList<>(); + q.offer(root); + // 为 true 时向右,false 时向左 + boolean flag = true; + + // while 循环控制从上向下一层层遍历 + while (!q.isEmpty()) { + int sz = q.size(); + // 记录这一层的节点值 + LinkedList level = new LinkedList<>(); + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + // 实现 z 字形遍历 + if (flag) { + level.addLast(cur.val); + } else { + level.addFirst(cur.val); + } + if (cur.left != null) + q.offer(cur.left); + if (cur.right != null) + q.offer(cur.right); + } + // 切换方向 + flag = !flag; + res.add(level); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var zigzagLevelOrder = function(root) { + let res = []; + if (root == null) { + return res; + } + + let q = []; + q.push(root); + // 为 true 时向右,false 时向左 + let flag = true; + + // while 循环控制从上向下一层层遍历 + while (q.length > 0) { + let sz = q.length; + // 记录这一层的节点值 + let level = []; + // for 循环控制每一层从左向右遍历 + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + // 实现 z 字形遍历 + if (flag) { + level.push(cur.val); + } else { + level.unshift(cur.val); + } + if (cur.left != null) + q.push(cur.left); + if (cur.right != null) + q.push(cur.right); + } + // 切换方向 + flag = !flag; + res.push(level); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]: + res = [] + if not root: + return res + + q = collections.deque() + q.append(root) + # 为 True 时向右,False 时向左 + flag = True + + # while 循环控制从上向下一层层遍历 + while q: + sz = len(q) + # 记录这一层的节点值 + level = collections.deque() + # for 循环控制每一层从左向右遍历 + for i in range(sz): + cur = q.popleft() + # 实现 z 字形遍历 + if flag: + level.append(cur.val) + else: + level.appendleft(cur.val) + if cur.left: + q.append(cur.left) + if cur.right: + q.append(cur.right) + # 切换方向 + flag = not flag + res.append(list(level)) + return res +``` + +https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal 的多语言解法👆 + +https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isStraight(vector& nums) { + unordered_set numSet; + int maxVal = INT_MIN, minVal = INT_MAX; + for (int num : nums) { + if (num == 0) { + // 小王不用管 + continue; + } + if (numSet.count(num)) { + // 出现重复,不可能是顺子 + return false; + } + numSet.insert(num); + + maxVal = max(maxVal, num); + minVal = min(minVal, num); + } + // 考虑到大小王,只要差小于 4 就一定可以凑出顺子 + return maxVal - minVal <= 4; + } +}; +``` + +```go +// by chatGPT (go) +func isStraight(nums []int) bool { + numSet := make(map[int]bool) + max, min := math.MinInt32, math.MaxInt32 + for _, num := range nums { + if num == 0 { + // 小王不用管 + continue + } + if numSet[num] { + // 出现重复,不可能是顺子 + return false + } + numSet[num] = true + + max = int(math.Max(float64(max), float64(num))) + min = int(math.Min(float64(min), float64(num))) + } + // 考虑到大小王,只要差小于 4 就一定可以凑出顺子 + return max - min <= 4 +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isStraight(int[] nums) { + Set numSet = new HashSet<>(); + int max = Integer.MIN_VALUE, min = Integer.MAX_VALUE; + for (int num : nums) { + if (num == 0) { + // 小王不用管 + continue; + } + if (numSet.contains(num)) { + // 出现重复,不可能是顺子 + return false; + } + numSet.add(num); + + max = Math.max(max, num); + min = Math.min(min, num); + } + // 考虑到大小王,只要差小于 4 就一定可以凑出顺子 + return max - min <= 4; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isStraight = function(nums) { + let numSet = new Set(); + let max = -Infinity, min = Infinity; + for (let num of nums) { + if (num === 0) { + // 小王不用管 + continue; + } + if (numSet.has(num)) { + // 出现重复,不可能是顺子 + return false; + } + numSet.add(num); + + max = Math.max(max, num); + min = Math.min(min, num); + } + // 考虑到大小王,只要差小于 4 就一定可以凑出顺子 + return max - min <= 4; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isStraight(self, nums: List[int]) -> bool: + numSet = set() + maximum, minimum = float('-inf'), float('inf') + for num in nums: + if num == 0: + # 小王不用管 + continue + if num in numSet: + # 出现重复,不可能是顺子 + return False + numSet.add(num) + maximum = max(maximum, num) + minimum = min(minimum, num) + # 考虑到大小王,只要差小于 4 就一定可以凑出顺子 + return maximum - minimum <= 4 +``` + +https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int add(int a, int b) { + if (a == 0 || b == 0) { + return a == 0 ? b : a; + } + // 设 a = 1001 + // 设 b = 0101 + // 求和 1100 + int sum = a ^ b; + // 进位 0001 << 1 = 0010 + int carry = (a & b) << 1; + // add(1100, 0010) + return add(sum, carry); + } +}; +``` + +```go +// by chatGPT (go) +func add(a int, b int) int { + if a == 0 || b == 0 { + if a == 0 { + return b + } else { + return a + } + } + + // 设 a = 1001 + // 设 b = 0101 + // 求和 1100 + sum := a ^ b + + // 进位 0001 << 1 = 0010 + carry := (a & b) << 1 + + // add(1100, 0010) + return add(sum, carry) +} +``` + +```java +// by labuladong (java) +class Solution { + public int add(int a, int b) { + if(a == 0 || b == 0) { + return a == 0 ? b : a; + } + // 设 a = 1001 + // 设 b = 0101 + // 求和 1100 + int sum = a ^ b; + // 进位 0001 << 1 = 0010 + int carry = (a & b) << 1; + // add(1100, 0010) + return add(sum, carry); + } +} +``` + +```javascript +// by chatGPT (javascript) +var add = function(a, b) { + if (a == 0 || b == 0) { + return a == 0 ? b : a; + } + // 设 a = 1001 + // 设 b = 0101 + // 求和 1100 + let sum = a ^ b; + // 进位 0001 << 1 = 0010 + let carry = (a & b) << 1; + // add(1100, 0010) + return add(sum, carry); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def add(self, a: int, b: int) -> int: + if a == 0 or b == 0: + return b if a == 0 else a + # 设 a = 1001 + # 设 b = 0101 + # 求和 1100 + sum = a ^ b + # 进位 0001 << 1 = 0010 + carry = (a & b) << 1 + # add(1100, 0010) + return self.add(sum, carry) +``` + +https://leetcode.cn/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof 的多语言解法👆 + +https://leetcode.cn/problems/bulb-switcher 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int bulbSwitch(int n) { + return (int) sqrt(n); + } +}; +``` + +```go +// by chatGPT (go) +func bulbSwitch(n int) int { + return int(math.Sqrt(float64(n))) +} +``` + +```java +// by labuladong (java) +class Solution { + public int bulbSwitch(int n) { + return (int)Math.sqrt(n); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number} + */ +var bulbSwitch = function(n) { + return Math.floor(Math.sqrt(n)); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def bulbSwitch(self, n: int) -> int: + return int(n ** 0.5) +``` + +https://leetcode.cn/problems/bulb-switcher 的多语言解法👆 + +https://leetcode.cn/problems/burst-balloons 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxCoins(vector& nums) { + int n = nums.size(); + // 添加两侧的虚拟气球 + vector points(n + 2); + points[0] = points[n + 1] = 1; + for (int i = 1; i <= n; i++) { + points[i] = nums[i - 1]; + } + // base case 已经都被初始化为 0 + vector> dp(n + 2, vector(n + 2, 0)); + // 开始状态转移 + // i 应该从下往上 + for (int i = n; i >= 0; i--) { + // j 应该从左往右 + for (int j = i + 1; j < n + 2; j++) { + // 最后戳破的气球是哪个? + for (int k = i + 1; k < j; k++) { + // 择优做选择 + dp[i][j] = max( + dp[i][j], + dp[i][k] + dp[k][j] + points[i] * points[j] * points[k] + ); + } + } + } + return dp[0][n + 1]; + } +}; +``` + +```go +// by chatGPT (go) +func maxCoins(nums []int) int { + n := len(nums) + // 添加两侧的虚拟气球 + points := make([]int, n+2) + points[0] = points[n+1] = 1 + for i := 1; i <= n; i++ { + points[i] = nums[i-1] + } + // base case 已经都被初始化为 0 + dp := make([][]int, n+2) + for i := 0; i <= n+1; i++ { + dp[i] = make([]int, n+2) + } + // 开始状态转移 + // i 应该从下往上 + for i := n; i >= 0; i-- { + // j 应该从左往右 + for j := i + 1; j < n+2; j++ { + // 最后戳破的气球是哪个? + for k := i + 1; k < j; k++ { + // 择优做选择 + dp[i][j] = max(dp[i][j], dp[i][k]+dp[k][j]+points[i]*points[j]*points[k]) + } + } + } + return dp[0][n+1] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxCoins(int[] nums) { + int n = nums.length; + // 添加两侧的虚拟气球 + int[] points = new int[n + 2]; + points[0] = points[n + 1] = 1; + for (int i = 1; i <= n; i++) { + points[i] = nums[i - 1]; + } + // base case 已经都被初始化为 0 + int[][] dp = new int[n + 2][n + 2]; + // 开始状态转移 + // i 应该从下往上 + for (int i = n; i >= 0; i--) { + // j 应该从左往右 + for (int j = i + 1; j < n + 2; j++) { + // 最后戳破的气球是哪个? + for (int k = i + 1; k < j; k++) { + // 择优做选择 + dp[i][j] = Math.max( + dp[i][j], + dp[i][k] + dp[k][j] + points[i] * points[j] * points[k] + ); + } + } + } + return dp[0][n + 1]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxCoins = function(nums) { + let n = nums.length; + // 添加两侧的虚拟气球 + let points = new Array(n + 2).fill(0); + points[0] = points[n + 1] = 1; + for (let i = 1; i <= n; i++) { + points[i] = nums[i - 1]; + } + // base case 已经都被初始化为 0 + let dp = new Array(n + 2).fill().map(() => new Array(n + 2).fill(0)); + // 开始状态转移 + // i 应该从下往上 + for (let i = n; i >= 0; i--) { + // j 应该从左往右 + for (let j = i + 1; j < n + 2; j++) { + // 最后戳破的气球是哪个? + for (let k = i + 1; k < j; k++) { + // 择优做选择 + dp[i][j] = Math.max( + dp[i][j], + dp[i][k] + dp[k][j] + points[i] * points[j] * points[k] + ); + } + } + } + return dp[0][n + 1]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxCoins(self, nums: List[int]) -> int: + n = len(nums) + # 添加两侧的虚拟气球 + points = [0] * (n + 2) + points[0], points[n + 1] = 1, 1 + for i in range(1, n + 1): + points[i] = nums[i - 1] + # base case 已经都被初始化为 0 + dp = [[0] * (n + 2) for _ in range(n + 2)] + # 开始状态转移 + # i 应该从下往上 + for i in range(n, -1, -1): + # j 应该从左往右 + for j in range(i + 1, n + 2): + # 最后戳破的气球是哪个? + for k in range(i + 1, j): + # 择优做选择 + dp[i][j] = max( + dp[i][j], + dp[i][k] + dp[k][j] + points[i] * points[j] * points[k] + ) + return dp[0][n + 1] +``` + +https://leetcode.cn/problems/burst-balloons 的多语言解法👆 + +https://leetcode.cn/problems/c32eOV 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode *detectCycle(ListNode *head) { + ListNode *fast, *slow; + fast = slow = head; + while (fast != nullptr && fast->next != nullptr) { + fast = fast->next->next; + slow = slow->next; + if (fast == slow) break; + /** + ![](../pictures/双指针/3.jpeg) + */ + } + // 上面的代码类似 hasCycle 函数 + if (fast == nullptr || fast->next == nullptr) { + // fast 遇到空指针说明没有环 + return nullptr; + } + + // 重新指向头结点 + slow = head; + /** + ![](../pictures/双指针/2.jpeg) + */ + // 快慢指针同步前进,相交点就是环起点 + while (slow != fast) { + fast = fast->next; + slow = slow->next; + } + return slow; + } +}; +``` + +```go +// by chatGPT (go) +func detectCycle(head *ListNode) *ListNode { + fast, slow := head, head + for fast != nil && fast.Next != nil { + fast = fast.Next.Next + slow = slow.Next + if fast == slow { + break + /** + ![](../pictures/双指针/3.jpeg) + */ + } + } + // 上面的代码类似 hasCycle 函数 + if fast == nil || fast.Next == nil { + // fast 遇到空指针说明没有环 + return nil + } + + // 重新指向头结点 + slow = head + /** + ![](../pictures/双指针/2.jpeg) + */ + // 快慢指针同步前进,相交点就是环起点 + for slow != fast { + fast = fast.Next + slow = slow.Next + } + return slow +} +``` + +```java +// by labuladong (java) +public class Solution { + public ListNode detectCycle(ListNode head) { + ListNode fast, slow; + fast = slow = head; + while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; + if (fast == slow) break; + /** + ![](../pictures/双指针/3.jpeg) + */ + } + // 上面的代码类似 hasCycle 函数 + if (fast == null || fast.next == null) { + // fast 遇到空指针说明没有环 + return null; + } + + // 重新指向头结点 + slow = head; + /** + ![](../pictures/双指针/2.jpeg) + */ + // 快慢指针同步前进,相交点就是环起点 + while (slow != fast) { + fast = fast.next; + slow = slow.next; + } + return slow; + } +} +``` + +```javascript +// by chatGPT (javascript) +var detectCycle = function(head) { + let fast, slow; + fast = slow = head; + while (fast !== null && fast.next !== null) { + fast = fast.next.next; + slow = slow.next; + if (fast === slow) break; + /** + ![](../pictures/双指针/3.jpeg) + */ + } + // 上面的代码类似 hasCycle 函数 + if (fast === null || fast.next === null) { + // fast 遇到空指针说明没有环 + return null; + } + + // 重新指向头结点 + slow = head; + /** + ![](../pictures/双指针/2.jpeg) + */ + // 快慢指针同步前进,相交点就是环起点 + while (slow !== fast) { + fast = fast.next; + slow = slow.next; + } + return slow; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def detectCycle(self, head: ListNode) -> ListNode: + fast, slow = head, head + while fast and fast.next: + fast = fast.next.next + slow = slow.next + if fast == slow: + break + """ + + ![](../pictures/双指针/3.jpeg) + """ + # 上面的代码类似 hasCycle 函数 + if not fast or not fast.next: + # fast 遇到空指针说明没有环 + return None + + # 重新指向头结点 + slow = head + """ + + ![](../pictures/双指针/2.jpeg) + """ + # 快慢指针同步前进,相交点就是环起点 + while slow != fast: + slow = slow.next + fast = fast.next + return slow +``` + +https://leetcode.cn/problems/c32eOV 的多语言解法👆 + +https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int shipWithinDays(vector& weights, int days) { + int left = 0; + int right = 1; + for (int w : weights) { + left = max(left, w); + right += w; + } + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(weights, mid) <= days) { + right = mid; + } else { + left = mid + 1; + } + } + + return left; + } + + // 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物 + // f(x) 随着 x 的增加单调递减 + int f(vector& weights, int x) { + int days = 0; + for (int i = 0; i < weights.size(); ) { + // 尽可能多装货物 + int cap = x; + while (i < weights.size()) { + if (cap < weights[i]) break; + else cap -= weights[i]; + i++; + } + days++; + } + return days; + } +}; +``` + +```go +// by chatGPT (go) +func shipWithinDays(weights []int, days int) int { + left := 0 + right := 1 + for _, w := range weights { + left = max(left, w) + right += w + } + + for left < right { + mid := left + (right-left)/2 + if f(weights, mid) <= days { + right = mid + } else { + left = mid + 1 + } + } + + return left +} + +// 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物 +// f(x) 随着 x 的增加单调递减 +func f(weights []int, x int) int { + days := 0 + for i := 0; i < len(weights); { + // 尽可能多装货物 + cap := x + for i < len(weights) { + if cap < weights[i] { + break + } else { + cap -= weights[i] + i++ + } + } + days++ + } + return days +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int shipWithinDays(int[] weights, int days) { + int left = 0; + int right = 1; + for (int w : weights) { + left = Math.max(left, w); + right += w; + } + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(weights, mid) <= days) { + right = mid; + } else { + left = mid + 1; + } + } + + return left; + } + + // 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物 + // f(x) 随着 x 的增加单调递减 + int f(int[] weights, int x) { + int days = 0; + for (int i = 0; i < weights.length; ) { + // 尽可能多装货物 + int cap = x; + while (i < weights.length) { + if (cap < weights[i]) break; + else cap -= weights[i]; + i++; + } + days++; + } + return days; + } +} +``` + +```javascript +// by chatGPT (javascript) +var shipWithinDays = function(weights, days) { + let left = 0; + let right = 1; + for (let w of weights) { + left = Math.max(left, w); + right += w; + } + + while (left < right) { + let mid = left + Math.floor((right - left) / 2); + if (f(weights, mid) <= days) { + right = mid; + } else { + left = mid + 1; + } + } + + return left; +}; + +// 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物 +// f(x) 随着 x 的增加单调递减 +function f(weights, x) { + let days = 0; + for (let i = 0; i < weights.length; ) { + // 尽可能多装货物 + let cap = x; + while (i < weights.length) { + if (cap < weights[i]) break; + else cap -= weights[i]; + i++; + } + days++; + } + return days; +} +``` + +```python +# by chatGPT (python) +class Solution: + def shipWithinDays(self, weights: List[int], days: int) -> int: + left = max(weights) + right = sum(weights) + + while left < right: + mid = (left + right) // 2 + if self.f(weights, mid) <= days: + right = mid + else: + left = mid + 1 + + return left + + # 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物 + # f(x) 随着 x 的增加单调递减 + def f(self, weights, x): + days = 0 + i = 0 + while i < len(weights): + # 尽可能多装货物 + cap = x + while i < len(weights): + if cap < weights[i]: + break + else: + cap -= weights[i] + i += 1 + days += 1 + return days +``` + +https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days 的多语言解法👆 + +https://leetcode.cn/problems/car-pooling 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool carPooling(vector>& trips, int capacity) { + // 最多有 1000 个车站 + vector nums(1001, 0); + // 构造差分解法 + Difference df(nums); + + for (const auto& trip : trips) { + // 乘客数量 + int val = trip[0]; + // 第 trip[1] 站乘客上车 + int i = trip[1]; + // 第 trip[2] 站乘客已经下车, + // 即乘客在车上的区间是 [trip[1], trip[2] - 1] + int j = trip[2] - 1; + // 进行区间操作 + df.increment(i, j, val); + } + + vector res = df.result(); + + // 客车自始至终都不应该超载 + for (int i = 0; i < res.size(); i++) { + if (capacity < res[i]) { + return false; + } + } + return true; + } + + // 差分数组工具类 + class Difference { + // 差分数组 + vector diff; + + /* 输入一个初始数组,区间操作将在这个数组上进行 */ + public: + Difference(vector& nums) { + diff.resize(nums.size()); + // 根据初始数组构造差分数组 + diff[0] = nums[0]; + for (int i = 1; i < nums.size(); i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.size()) { + diff[j + 1] -= val; + } + } + + /* 返回结果数组 */ + vector result() { + vector res(diff.size()); + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (int i = 1; i < diff.size(); i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } + }; +}; +``` + +```go +// by chatGPT (go) +func carPooling(trips [][]int, capacity int) bool { + // 最多有 1000 个车站 + nums := make([]int, 1001) + // 构造差分解法 + df := Difference{nums} + + for _, trip := range trips { + // 乘客数量 + val := trip[0] + // 第 trip[1] 站乘客上车 + i := trip[1] + // 第 trip[2] 站乘客已经下车, + // 即乘客在车上的区间是 [trip[1], trip[2] - 1] + j := trip[2] - 1 + // 进行区间操作 + df.increment(i, j, val) + } + + res := df.result() + + // 客车自始至终都不应该超载 + for _, val := range res { + if capacity < val { + return false + } + } + return true +} + +// 差分数组工具类 +type Difference struct { + // 差分数组 + diff []int +} + +/* 输入一个初始数组,区间操作将在这个数组上进行 */ +func (df *Difference) Difference(nums []int) { + df.diff = make([]int, len(nums)) + // 根据初始数组构造差分数组 + df.diff[0] = nums[0] + for i := 1; i < len(nums); i++ { + df.diff[i] = nums[i] - nums[i-1] + } +} + +/* 给闭区间 [i, j] 增加 val(可以是负数)*/ +func (df *Difference) increment(i int, j int, val int) { + df.diff[i] += val + if j+1 < len(df.diff) { + df.diff[j+1] -= val + } +} + +/* 返回结果数组 */ +func (df *Difference) result() []int { + res := make([]int, len(df.diff)) + // 根据差分数组构造结果数组 + res[0] = df.diff[0] + for i := 1; i < len(df.diff); i++ { + res[i] = res[i-1] + df.diff[i] + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean carPooling(int[][] trips, int capacity) { + // 最多有 1000 个车站 + int[] nums = new int[1001]; + // 构造差分解法 + Difference df = new Difference(nums); + + for (int[] trip : trips) { + // 乘客数量 + int val = trip[0]; + // 第 trip[1] 站乘客上车 + int i = trip[1]; + // 第 trip[2] 站乘客已经下车, + // 即乘客在车上的区间是 [trip[1], trip[2] - 1] + int j = trip[2] - 1; + // 进行区间操作 + df.increment(i, j, val); + } + + int[] res = df.result(); + + // 客车自始至终都不应该超载 + for (int i = 0; i < res.length; i++) { + if (capacity < res[i]) { + return false; + } + } + return true; + } + + // 差分数组工具类 + class Difference { + // 差分数组 + private int[] diff; + + /* 输入一个初始数组,区间操作将在这个数组上进行 */ + public Difference(int[] nums) { + assert nums.length > 0; + diff = new int[nums.length]; + // 根据初始数组构造差分数组 + diff[0] = nums[0]; + for (int i = 1; i < nums.length; i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + public void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.length) { + diff[j + 1] -= val; + } + } + + /* 返回结果数组 */ + public int[] result() { + int[] res = new int[diff.length]; + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (int i = 1; i < diff.length; i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } + } + +} +``` + +```javascript +// by chatGPT (javascript) +var carPooling = function(trips, capacity) { + // 最多有 1000 个车站 + const nums = new Array(1001).fill(0); + // 构造差分解法 + const df = new Difference(nums); + + for (const trip of trips) { + // 乘客数量 + const val = trip[0]; + // 第 trip[1] 站乘客上车 + const i = trip[1]; + // 第 trip[2] 站乘客已经下车, + // 即乘客在车上的区间是 [trip[1], trip[2] - 1] + const j = trip[2] - 1; + // 进行区间操作 + df.increment(i, j, val); + } + + const res = df.result(); + + // 客车自始至终都不应该超载 + for (let i = 0; i < res.length; i++) { + if (capacity < res[i]) { + return false; + } + } + return true; + + // 差分数组工具类 + function Difference(nums) { + // 差分数组 + let diff = new Array(nums.length).fill(0); + + /* 输入一个初始数组,区间操作将在这个数组上进行 */ + this.increment = function(i, j, val) { + diff[i] += val; + if (j + 1 < diff.length) { + diff[j + 1] -= val; + } + } + + /* 返回结果数组 */ + this.result = function() { + const res = new Array(diff.length).fill(0); + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (let i = 1; i < diff.length; i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } + + // 根据初始数组构造差分数组 + diff[0] = nums[0]; + for (let i = 1; i < nums.length; i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def carPooling(self, trips: List[List[int]], capacity: int) -> bool: + # 最多有 1000 个车站 + nums = [0] * 1001 + # 构造差分解法 + df = self.Difference(nums) + + for trip in trips: + # 乘客数量 + val = trip[0] + # 第 trip[1] 站乘客上车 + i = trip[1] + # 第 trip[2] 站乘客已经下车, + # 即乘客在车上的区间是 [trip[1], trip[2] - 1] + j = trip[2] - 1 + # 进行区间操作 + df.increment(i, j, val) + + res = df.result() + + # 客车自始至终都不应该超载 + for i in range(len(res)): + if capacity < res[i]: + return False + return True + + # 差分数组工具类 + class Difference: + # 差分数组 + diff = [] + + """输入一个初始数组,区间操作将在这个数组上进行""" + def __init__(self, nums): + assert len(nums) > 0 + self.diff = [0] * len(nums) + # 根据初始数组构造差分数组 + self.diff[0] = nums[0] + for i in range(1, len(nums)): + self.diff[i] = nums[i] - nums[i - 1] + + """给闭区间 [i, j] 增加 val(可以是负数)""" + def increment(self, i, j, val): + self.diff[i] += val + if j + 1 < len(self.diff): + self.diff[j + 1] -= val + + """返回结果数组""" + def result(self): + res = [0] * len(self.diff) + # 根据差分数组构造结果数组 + res[0] = self.diff[0] + for i in range(1, len(self.diff)): + res[i] = res[i - 1] + self.diff[i] + return res +``` + +https://leetcode.cn/problems/car-pooling 的多语言解法👆 + +https://leetcode.cn/problems/cheapest-flights-within-k-stops 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + unordered_map>> indegree; + int src, dst; + // 备忘录 + vector> memo; + +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int K) { + // 将中转站个数转化成边的条数 + K++; + this->src = src; + this->dst = dst; + // 初始化备忘录,全部填一个特殊值 + memo = vector>(n, vector(K + 1, -888)); + + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + // 记录谁指向该节点,以及之间的权重 + indegree[to].push_back(vector{from, price}); + } + + return dp(dst, K); + } + + // 定义:从 src 出发,k 步之内到达 s 的最短路径权重 + int dp(int s, int k) { + // base case + if (s == src) { + return 0; + } + if (k == 0) { + return -1; + } + // 查备忘录,防止冗余计算 + if (memo[s][k] != -888) { + return memo[s][k]; + } + + // 初始化为最大值,方便等会取最小值 + int res = INT_MAX; + if (indegree.count(s)) { + // 当 s 有入度节点时,分解为子问题 + auto &v = indegree[s]; + for (auto &p : v) { + int from = p[0]; + int price = p[1]; + // 从 src 到达相邻的入度节点所需的最短路径权重 + int subProblem = dp(from, k - 1); + /** + ![](../pictures/旅行最短路径/4.jpeg) + */ + // 跳过无解的情况 + if (subProblem != -1) { + res = min(res, subProblem + price); + } + } + } + // 存入备忘录 + memo[s][k] = res == INT_MAX ? -1 : res; + return memo[s][k]; + } +}; +``` + +```go +// by chatGPT (go) +func findCheapestPrice(n int, flights [][]int, src int, dst int, K int) int { + // 将中转站个数转化成边的条数 + K++ + // 初始化备忘录,全部填一个特殊值 + memo := make([][]int, n) + for i := range memo { + memo[i] = make([]int, K+1) + for j := range memo[i] { + memo[i][j] = -888 + } + } + + indegree := make(map[int][][]int) + for _, f := range flights { + from, to, price := f[0], f[1], f[2] + // 记录谁指向该节点,以及之间的权重 + indegree[to] = append(indegree[to], []int{from, price}) + } + + return dp(dst, K, src, memo, indegree) +} + +// 定义:从 src 出发,k 步之内到达 s 的最短路径权重 +func dp(s int, k int, src int, memo [][]int, indegree map[int][][]int) int { + // base case + if s == src { + return 0 + } + if k == 0 { + return -1 + } + // 查备忘录,防止冗余计算 + if memo[s][k] != -888 { + return memo[s][k] + } + + // 初始化为最大值,方便等会取最小值 + res := math.MaxInt32 + if v, ok := indegree[s]; ok { + // 当 s 有入度节点时,分解为子问题 + for _, edge := range v { + from, price := edge[0], edge[1] + // 从 src 到达相邻的入度节点所需的最短路径权重 + subProblem := dp(from, k-1, src, memo, indegree) + // 跳过无解的情况 + if subProblem != -1 { + res = min(res, subProblem+price) + } + } + } + // 存入备忘录 + if res == math.MaxInt32 { + memo[s][k] = -1 + } else { + memo[s][k] = res + } + return memo[s][k] +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + HashMap> indegree; + int src, dst; + // 备忘录 + int[][] memo; + + public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) { + // 将中转站个数转化成边的条数 + K++; + this.src = src; + this.dst = dst; + // 初始化备忘录,全部填一个特殊值 + memo = new int[n][K + 1]; + for (int[] row : memo) { + Arrays.fill(row, -888); + } + + indegree = new HashMap<>(); + for (int[] f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + // 记录谁指向该节点,以及之间的权重 + indegree.putIfAbsent(to, new LinkedList<>()); + indegree.get(to).add(new int[]{from, price}); + } + + return dp(dst, K); + } + + // 定义:从 src 出发,k 步之内到达 s 的最短路径权重 + int dp(int s, int k) { + // base case + if (s == src) { + return 0; + } + if (k == 0) { + return -1; + } + // 查备忘录,防止冗余计算 + if (memo[s][k] != -888) { + return memo[s][k]; + } + + // 初始化为最大值,方便等会取最小值 + int res = Integer.MAX_VALUE; + if (indegree.containsKey(s)) { + // 当 s 有入度节点时,分解为子问题 + for (int[] v : indegree.get(s)) { + int from = v[0]; + int price = v[1]; + // 从 src 到达相邻的入度节点所需的最短路径权重 + int subProblem = dp(from, k - 1); + /** + ![](../pictures/旅行最短路径/4.jpeg) + */ + // 跳过无解的情况 + if (subProblem != -1) { + res = Math.min(res, subProblem + price); + } + } + } + // 存入备忘录 + memo[s][k] = res == Integer.MAX_VALUE ? -1 : res; + return memo[s][k]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findCheapestPrice = function(n, flights, src, dst, K) { + // 将中转站个数转化成边的条数 + K++; + // 定义备忘录 + const memo = Array.from({ length: n }, () => Array(K + 1).fill(-888)); + // 初始化起点和终点 + const [source, target] = [src, dst]; + + // 构建入度字典 + const indegree = new Map(); + for (const [from, to, price] of flights) { + indegree.set(to, (indegree.get(to) || []).concat([[from, price]])); + } + + function dp(s, k) { + // base case + if (s === source) { + return 0; + } + if (k === 0) { + return -1; + } + // 查备忘录,防止冗余计算 + if (memo[s][k] !== -888) { + return memo[s][k]; + } + + // 初始化为最大值,方便等会取最小值 + let res = Infinity; + if (indegree.has(s)) { + // 当 s 有入度节点时,分解为子问题 + for (const [from, price] of indegree.get(s)) { + // 从 src 到达相邻的入度节点所需的最短路径权重 + const subProblem = dp(from, k - 1); + /** + ![](../pictures/旅行最短路径/4.jpeg) + */ + // 跳过无解的情况 + if (subProblem !== -1) { + res = Math.min(res, subProblem + price); + } + } + } + // 存入备忘录 + memo[s][k] = res === Infinity ? -1 : res; + return memo[s][k]; + } + + return dp(target, K); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, K: int) -> int: + # 将中转站个数转化成边的条数 + K += 1 + # 初始化备忘录,全部填一个特殊值 + memo = [[-888] * (K + 1) for _ in range(n)] + # 定义入度字典 + indegree = {} + for f in flights: + from_, to, price = f + # 记录谁指向该节点,以及之间的权重 + if to not in indegree: + indegree[to] = [] + indegree[to].append([from_, price]) + + # 定义:从 src 出发,k 步之内到达 s 的最短路径权重 + def dp(s: int, k: int) -> int: + # base case + if s == src: + return 0 + if k == 0: + return -1 + # 查备忘录,防止冗余计算 + if memo[s][k] != -888: + return memo[s][k] + + # 初始化为最大值,方便等会取最小值 + res = float('inf') + if s in indegree: + # 当 s 有入度节点时,分解为子问题 + for v in indegree[s]: + from_, price = v + # 从 src 到达相邻的入度节点所需的最短路径权重 + sub_problem = dp(from_, k - 1) + # 跳过无解的情况 + if sub_problem != -1: + res = min(res, sub_problem + price) + + # 存入备忘录 + memo[s][k] = -1 if res == float('inf') else res + return memo[s][k] + + return dp(dst, K) +``` + +https://leetcode.cn/problems/cheapest-flights-within-k-stops 的多语言解法👆 + +https://leetcode.cn/problems/chou-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int nthUglyNumber(int n) { + // 可以理解为三个指向有序链表头结点的指针 + int p2 = 1, p3 = 1, p5 = 1; + // 可以理解为三个有序链表的头节点的值 + int product2 = 1, product3 = 1, product5 = 1; + // 可以理解为最终合并的有序链表(结果链表) + int ugly[n + 1]; + // 可以理解为结果链表上的指针 + int p = 1; + + // 开始合并三个有序链表 + while (p <= n) { + // 取三个链表的最小结点 + int minVal = min({product2, product3, product5}); + // 接到结果链表上 + ugly[p] = minVal; + p++; + // 前进对应有序链表上的指针 + if (minVal == product2) { + product2 = 2 * ugly[p2]; + p2++; + } + if (minVal == product3) { + product3 = 3 * ugly[p3]; + p3++; + } + if (minVal == product5) { + product5 = 5 * ugly[p5]; + p5++; + } + } + // 返回第 n 个丑数 + return ugly[n]; + } +}; +``` + +```go +// by chatGPT (go) +func nthUglyNumber(n int) int { + // 可以理解为三个指向有序链表头结点的指针 + p2, p3, p5 := 1, 1, 1 + // 可以理解为三个有序链表的头节点的值 + product2, product3, product5 := 1, 1, 1 + // 可以理解为最终合并的有序链表(结果链表) + ugly := make([]int, n+1) + // 可以理解为结果链表上的指针 + p := 1 + + // 开始合并三个有序链表 + for p <= n { + // 取三个链表的最小结点 + min := min(min(product2, product3), product5) + // 接到结果链表上 + ugly[p] = min + p++ + // 前进对应有序链表上的指针 + if min == product2 { + product2 = 2 * ugly[p2] + p2++ + } + if min == product3 { + product3 = 3 * ugly[p3] + p3++ + } + if min == product5 { + product5 = 5 * ugly[p5] + p5++ + } + } + // 返回第 n 个丑数 + return ugly[n] +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int nthUglyNumber(int n) { + // 可以理解为三个指向有序链表头结点的指针 + int p2 = 1, p3 = 1, p5 = 1; + // 可以理解为三个有序链表的头节点的值 + int product2 = 1, product3 = 1, product5 = 1; + // 可以理解为最终合并的有序链表(结果链表) + int[] ugly = new int[n + 1]; + // 可以理解为结果链表上的指针 + int p = 1; + + // 开始合并三个有序链表 + while (p <= n) { + // 取三个链表的最小结点 + int min = Math.min(Math.min(product2, product3), product5); + // 接到结果链表上 + ugly[p] = min; + p++; + // 前进对应有序链表上的指针 + if (min == product2) { + product2 = 2 * ugly[p2]; + p2++; + } + if (min == product3) { + product3 = 3 * ugly[p3]; + p3++; + } + if (min == product5) { + product5 = 5 * ugly[p5]; + p5++; + } + } + // 返回第 n 个丑数 + return ugly[n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var nthUglyNumber = function(n) { + // 可以理解为三个指向有序链表头结点的指针 + let p2 = 1, p3 = 1, p5 = 1; + // 可以理解为三个有序链表的头节点的值 + let product2 = 1, product3 = 1, product5 = 1; + // 可以理解为最终合并的有序链表(结果链表) + let ugly = new Array(n+1); + // 可以理解为结果链表上的指针 + let p = 1; + + // 开始合并三个有序链表 + while (p <= n) { + // 取三个链表的最小结点 + let min = Math.min(Math.min(product2, product3), product5); + // 接到结果链表上 + ugly[p] = min; + p++; + // 前进对应有序链表上的指针 + if (min === product2) { + product2 = 2 * ugly[p2]; + p2++; + } + if (min === product3) { + product3 = 3 * ugly[p3]; + p3++; + } + if (min === product5) { + product5 = 5 * ugly[p5]; + p5++; + } + } + // 返回第 n 个丑数 + return ugly[n]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def nthUglyNumber(self, n: int) -> int: + # 可以理解为三个指向有序链表头结点的指针 + p2, p3, p5 = 1, 1, 1 + # 可以理解为三个有序链表的头节点的值 + product2, product3, product5 = 1, 1, 1 + # 可以理解为最终合并的有序链表(结果链表) + ugly = [0] * (n+1) + # 可以理解为结果链表上的指针 + p = 1 + + # 开始合并三个有序链表 + while p <= n: + # 取三个链表的最小结点 + min_val = min(product2, product3, product5) + # 接到结果链表上 + ugly[p] = min_val + p += 1 + # 前进对应有序链表上的指针 + if min_val == product2: + product2 = 2 * ugly[p2] + p2 += 1 + if min_val == product3: + product3 = 3 * ugly[p3] + p3 += 1 + if min_val == product5: + product5 = 5 * ugly[p5] + p5 += 1 + + # 返回第 n 个丑数 + return ugly[n] +``` + +https://leetcode.cn/problems/chou-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/climbing-stairs 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 备忘录 + vector memo; + + int climbStairs(int n) { + memo.resize(n + 1, 0); + return dp(n); + } + + // 定义:爬到第 n 级台阶的方法个数为 dp(n) + int dp(int n) { + // base case + if (n <= 2) { + return n; + } + if (memo[n] > 0) { + return memo[n]; + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = dp(n - 1) + dp(n - 2); + return memo[n]; + } +}; +``` + +```go +// by chatGPT (go) +func climbStairs(n int) int { + memo := make([]int, n+1) + var dp func(int) int + dp = func(n int) int { + // base case + if n <= 2 { + return n + } + if memo[n] > 0 { + return memo[n] + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = dp(n-1) + dp(n-2) + return memo[n] + } + return dp(n) +} +``` + +```java +// by labuladong (java) +class Solution { + // 备忘录 + int[] memo; + + public int climbStairs(int n) { + memo = new int[n + 1]; + return dp(n); + } + + // 定义:爬到第 n 级台阶的方法个数为 dp(n) + int dp(int n) { + // base case + if (n <= 2) { + return n; + } + if (memo[n] > 0) { + return memo[n]; + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = dp(n - 1) + dp(n - 2); + return memo[n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var climbStairs = function(n) { + // 备忘录 + const memo = new Array(n + 1).fill(0); + return dp(n); + + // 定义:爬到第 n 级台阶的方法个数为 dp(n) + function dp(n) { + // base case + if (n <= 2) { + return n; + } + if (memo[n] > 0) { + return memo[n]; + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = dp(n - 1) + dp(n - 2); + return memo[n]; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + # 备忘录 + memo = [] + + def climbStairs(self, n: int) -> int: + self.memo = [0] * (n + 1) + return self.dp(n) + + # 定义:爬到第 n 级台阶的方法个数为 dp(n) + def dp(self, n: int) -> int: + # base case + if n <= 2: + return n + if self.memo[n] > 0: + return self.memo[n] + # 状态转移方程: + # 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + self.memo[n] = self.dp(n - 1) + self.dp(n - 2) + return self.memo[n] +``` + +https://leetcode.cn/problems/climbing-stairs 的多语言解法👆 + +https://leetcode.cn/problems/coin-change 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + vector memo; + + public: + int coinChange(vector& coins, int amount) { + memo.resize(amount + 1); + // dp 数组全都初始化为特殊值 + fill(memo.begin(), memo.end(), -666); + return dp(coins, amount); + } + + int dp(vector& coins, int amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; + + int res = INT_MAX; + for (int coin : coins) { + // 计算子问题的结果 + int subProblem = dp(coins, amount - coin); + /** + ![](../pictures/动态规划详解进阶/5.jpg) + */ + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = min(res, subProblem + 1); + } + // 把计算结果存入备忘录 + memo[amount] = (res == INT_MAX) ? -1 : res; + return memo[amount]; + } +}; +``` + +```go +// by chatGPT (go) +func coinChange(coins []int, amount int) int { + memo := make([]int, amount+1) + for i := 0; i <= amount; i++ { + memo[i] = -666 + } + return dp(coins, amount, memo) +} + +func dp(coins []int, amount int, memo []int) int { + if amount == 0 { + return 0 + } + if amount < 0 { + return -1 + } + // 查备忘录,防止重复计算 + if memo[amount] != -666 { + return memo[amount] + } + + res := math.MaxInt32 + for _, coin := range coins { + // 计算子问题的结果 + subProblem := dp(coins, amount-coin, memo) + // 子问题无解则跳过 + if subProblem == -1 { + continue + } + // 在子问题中选择最优解,然后加一 + res = min(res, subProblem+1) + } + // 把计算结果存入备忘录 + if res == math.MaxInt32 { + memo[amount] = -1 + } else { + memo[amount] = res + } + return memo[amount] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + int[] memo; + + public int coinChange(int[] coins, int amount) { + memo = new int[amount + 1]; + // dp 数组全都初始化为特殊值 + Arrays.fill(memo, -666); + return dp(coins, amount); + } + + int dp(int[] coins, int amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; + + int res = Integer.MAX_VALUE; + for (int coin : coins) { + // 计算子问题的结果 + int subProblem = dp(coins, amount - coin); + /** + ![](../pictures/动态规划详解进阶/5.jpg) + */ + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); + } + // 把计算结果存入备忘录 + memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; + return memo[amount]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var coinChange = function(coins, amount) { + let memo = new Array(amount + 1).fill(-666); + return dp(coins, amount); + + function dp(coins, amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; + + let res = Number.MAX_SAFE_INTEGER; + for (let coin of coins) { + // 计算子问题的结果 + let subProblem = dp(coins, amount - coin); + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); + } + // 把计算结果存入备忘录 + memo[amount] = (res == Number.MAX_SAFE_INTEGER) ? -1 : res; + return memo[amount]; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.memo = [] + + def coinChange(self, coins: List[int], amount: int) -> int: + self.memo = [-666] * (amount + 1) + return self.dp(coins, amount) + + def dp(self, coins: List[int], amount: int) -> int: + if amount == 0: + return 0 + if amount < 0: + return -1 + # 查备忘录,防止重复计算 + if self.memo[amount] != -666: + return self.memo[amount] + + res = float("inf") + for coin in coins: + # 计算子问题的结果 + sub_problem = self.dp(coins, amount - coin) + # 子问题无解则跳过 + if sub_problem == -1: + continue + # 在子问题中选择最优解,然后加一 + res = min(res, sub_problem + 1) + + # 把计算结果存入备忘录 + self.memo[amount] = -1 if res == float("inf") else res + return self.memo[amount] +``` + +https://leetcode.cn/problems/coin-change 的多语言解法👆 + +https://leetcode.cn/problems/coin-change-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int change(int amount, vector& coins) { + int n = coins.size(); + vector> dp(n + 1, vector(amount + 1)); + // base case + for (int i = 0; i <= n; i++) + dp[i][0] = 1; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= amount; j++) + if (j - coins[i-1] >= 0) + dp[i][j] = dp[i - 1][j] + + dp[i][j - coins[i-1]]; + else + dp[i][j] = dp[i - 1][j]; + } + return dp[n][amount]; + } +}; +``` + +```go +// by chatGPT (go) +func change(amount int, coins []int) int { + n := len(coins) + dp := make([][]int, n+1) + for i := range dp { + dp[i] = make([]int, amount+1) + dp[i][0] = 1 + } + + for i := 1; i <= n; i++ { + for j := 1; j <= amount; j++ { + if j-coins[i-1] >= 0 { + dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]] + } else { + dp[i][j] = dp[i-1][j] + } + } + } + + return dp[n][amount] +} +``` + +```java +// by labuladong (java) +class Solution { + public int change(int amount, int[] coins) { + int n = coins.length; + int[][] dp = new int[n + 1][amount + 1]; + // base case + for (int i = 0; i <= n; i++) + dp[i][0] = 1; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= amount; j++) + if (j - coins[i-1] >= 0) + dp[i][j] = dp[i - 1][j] + + dp[i][j - coins[i-1]]; + else + dp[i][j] = dp[i - 1][j]; + } + return dp[n][amount]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var change = function(amount, coins) { + let n = coins.length; + let dp = Array.from(Array(n + 1), () => Array(amount + 1).fill(0)); + // base case + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= amount; j++) { + if (j - coins[i-1] >= 0) { + dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i-1]]; + } else { + dp[i][j] = dp[i - 1][j]; + } + } + } + return dp[n][amount]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def change(self, amount: int, coins: List[int]) -> int: + n = len(coins) + dp = [[0]*(amount + 1) for i in range(n+1)] + # base case + for i in range(n+1): + dp[i][0] = 1 + + for i in range(1, n+1): + for j in range(1, amount+1): + if j - coins[i-1] >= 0: + dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i-1]] + else: + dp[i][j] = dp[i - 1][j] + return dp[n][amount] +``` + +https://leetcode.cn/problems/coin-change-ii 的多语言解法👆 + +https://leetcode.cn/problems/combination-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> res; + + vector> combinationSum(vector& candidates, int target) { + if (candidates.size() == 0) { + return res; + } + backtrack(candidates, 0, target, 0); + return res; + } + + // 记录回溯的路径 + vector track; + + // 回溯算法主函数 + void backtrack(vector& candidates, int start, int target, int sum) { + if (sum == target) { + // 找到目标和 + res.push_back(track); + return; + } + + if (sum > target) { + // 超过目标和,直接结束 + return; + } + + // 回溯算法框架 + for (int i = start; i < candidates.size(); i++) { + // 选择 candidates[i] + track.push_back(candidates[i]); + sum += candidates[i]; + // 递归遍历下一层回溯树 + backtrack(candidates, i, target, sum); + // 撤销选择 candidates[i] + sum -= candidates[i]; + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +func combinationSum(candidates []int, target int) [][]int { + var res [][]int + + // 定义回溯函数 + var backtrack func(start int, target int, sum int, track []int) + + backtrack = func(start int, target int, sum int, track []int) { + // 如果当前 sum 等于 target,说明已找到符合要求的组合 + if sum == target { + // 由于 track 数组为引用类型,因此需要重新生成一个数组对象来保存到结果集 + res = append(res, append([]int{}, track...)) + return + } + + // 如果当前 sum 已经大于 target,回溯结束 + if sum > target { + return + } + + // 从指定位置开始遍历候选数组 + for i := start; i < len(candidates); i++ { + // 将候选数组当前元素加入路径 + track = append(track, candidates[i]) + sum += candidates[i] + // 继续遍历下一个元素 + backtrack(i, target, sum, track) + // 回溯:将当前元素从路径中移除 + sum -= candidates[i] + track = track[:len(track)-1] + } + } + + backtrack(0, target, 0, []int{}) + + return res +} +``` + +```java +// by labuladong (java) +class Solution { + List> res = new LinkedList<>(); + + public List> combinationSum(int[] candidates, int target) { + if (candidates.length == 0) { + return res; + } + backtrack(candidates, 0, target, 0); + return res; + } + + // 记录回溯的路径 + LinkedList track = new LinkedList<>(); + + // 回溯算法主函数 + void backtrack(int[] candidates, int start, int target, int sum) { + if (sum == target) { + // 找到目标和 + res.add(new LinkedList<>(track)); + return; + } + + if (sum > target) { + // 超过目标和,直接结束 + return; + } + + // 回溯算法框架 + for (int i = start; i < candidates.length; i++) { + // 选择 candidates[i] + track.add(candidates[i]); + sum += candidates[i]; + // 递归遍历下一层回溯树 + backtrack(candidates, i, target, sum); + // 撤销选择 candidates[i] + sum -= candidates[i]; + track.removeLast(); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var combinationSum = function(candidates, target) { + let res = []; + let track = []; + + backtrack(candidates, 0, target, 0, track); + + return res; + + function backtrack(candidates, start, target, sum, track) { + if (sum === target) { + // 找到目标和 + res.push([...track]); + return; + } + + if (sum > target) { + // 超过目标和,直接结束 + return; + } + + // 回溯算法框架 + for (let i = start; i < candidates.length; i++) { + // 选择 candidates[i] + track.push(candidates[i]); + sum += candidates[i]; + // 递归遍历下一层回溯树 + backtrack(candidates, i, target, sum, track); + // 撤销选择 candidates[i] + sum -= candidates[i]; + track.pop(); + } + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + + def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: + if not candidates: + return self.res + self.backtrack(candidates, 0, target, 0) + return self.res + + # 记录回溯的路径 + track = [] + + # 回溯算法主函数 + def backtrack(self, candidates, start, target, sum): + if sum == target: + # 找到目标和 + self.res.append(self.track[:]) + return + + if sum > target: + # 超过目标和,直接结束 + return + + # 回溯算法框架 + for i in range(start, len(candidates)): + # 选择 candidates[i] + self.track.append(candidates[i]) + sum += candidates[i] + # 递归遍历下一层回溯树 + self.backtrack(candidates, i, target, sum) + # 撤销选择 candidates[i] + sum -= candidates[i] + self.track.pop() +``` + +https://leetcode.cn/problems/combination-sum 的多语言解法👆 + +https://leetcode.cn/problems/combinations 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + + vector>res; + vector> combine(int n, int k) { + if (k <= 0 || n <= 0) return res; + vector track; + backtrack(n, k, 1, track); + return res; + } + + void backtrack(int n, int k, int start, vector& track) { + // 到达树的底部 + if (k == track.size()) { + res.push_back(track); + return; + } + // 注意 i 从 start 开始递增 + for (int i = start; i <= n; i++) { + // 做选择 + track.push_back(i); + backtrack(n, k, i + 1, track); + // 撤销选择 + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +func combine(n int, k int) [][]int { + res := [][]int{} + if k <= 0 || n <= 0 { + return res + } + track := []int{} + backtrack := func(n, k, start int, track []int) {} + backtrack = func(n, k, start int, track []int) { + // 到达树的底部 + if k == len(track) { + ans := make([]int, k) + copy(ans, track) + res = append(res, ans) + return + } + // 注意 i 从 start 开始递增 + for i := start; i <= n; i++ { + // 做选择 + track = append(track, i) + backtrack(n, k, i+1, track) + // 撤销选择 + track = track[:len(track)-1] + } + } + backtrack(n, k, 1, track) + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + List> res = new LinkedList<>(); + + public List> combine(int n, int k) { + if (k <= 0 || n <= 0) { + return res; + } + List track = new LinkedList<>(); + backtrack(n, k, 1, track); + return res; + } + + void backtrack(int n, int k, int start, List track) { + // 到达树的底部 + if (k == track.size()) { + res.add(new LinkedList<>(track)); + return; + } + // 注意 i 从 start 开始递增 + for (int i = start; i <= n; i++) { + // 做选择 + track.add(i); + backtrack(n, k, i + 1, track); + // 撤销选择 + track.remove(track.size() - 1); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var combine = function(n, k) { + let res = []; + if (k <= 0 || n <= 0) return res; + let track = []; + backtrack(n, k, 1, track, res); + return res; +}; + +var backtrack = function(n, k, start, track, res) { + // 到达树的底部 + if (k === track.length) { + res.push([...track]); + return; + } + // 注意 i 从 start 开始递增 + for (let i = start; i <= n; i++) { + // 做选择 + track.push(i); + backtrack(n, k, i + 1, track, res); + // 撤销选择 + track.pop(); + } +}; + +// Example usage +console.log(combine(4, 2)); // [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]] +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + + def combine(self, n: int, k: int) -> List[List[int]]: + if k <= 0 or n <= 0: + return self.res + track = [] + self.backtrack(n, k, 1, track) + return self.res + + def backtrack(self, n: int, k: int, start: int, track: List[int]) -> None: + # 到达树的底部 + if k == len(track): + self.res.append(track[:]) + return + # 注意 i 从 start 开始递增 + for i in range(start, n + 1): + # 做选择 + track.append(i) + self.backtrack(n, k, i + 1, track) + # 撤销选择 + track.pop() +``` + +https://leetcode.cn/problems/combinations 的多语言解法👆 + +https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> levelOrder(TreeNode* root) { + vector> res; + if (root == nullptr) { + return res; + } + + queue q; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (!q.empty()) { + int sz = q.size(); + // 记录这一层的节点值 + vector level; + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode* cur = q.front(); + q.pop(); + level.push_back(cur->val); + if (cur->left != nullptr) + q.push(cur->left); + if (cur->right != nullptr) + q.push(cur->right); + } + res.push_back(level); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func levelOrder(root *TreeNode) [][]int { + res := [][]int{} + if root == nil { + return res + } + + q := []*TreeNode{root} + // while 循环控制从上向下一层层遍历 + for len(q) != 0 { + sz := len(q) + // 记录这一层的节点值 + level := []int{} + // for 循环控制每一层从左向右遍历 + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + level = append(level, cur.Val) + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + res = append(res, level) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> levelOrder(TreeNode root) { + List> res = new LinkedList<>(); + if (root == null) { + return res; + } + + Queue q = new LinkedList<>(); + q.offer(root); + // while 循环控制从上向下一层层遍历 + while (!q.isEmpty()) { + int sz = q.size(); + // 记录这一层的节点值 + List level = new LinkedList<>(); + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + level.add(cur.val); + if (cur.left != null) + q.offer(cur.left); + if (cur.right != null) + q.offer(cur.right); + } + res.add(level); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var levelOrder = function(root) { + let res = []; + if (root == null) { + return res; + } + + let q = []; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (q.length > 0) { + let sz = q.length; + // 记录这一层的节点值 + let level = []; + // for 循环控制每一层从左向右遍历 + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + level.push(cur.val); + if (cur.left != null) + q.push(cur.left); + if (cur.right != null) + q.push(cur.right); + } + res.push(level); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def levelOrder(self, root: TreeNode) -> List[List[int]]: + res = [] + if not root: + return res + + q = collections.deque() + q.append(root) + # while 循环控制从上向下一层层遍历 + while q: + sz = len(q) + # 记录这一层的节点值 + level = [] + # for 循环控制每一层从左向右遍历 + for i in range(sz): + cur = q.popleft() + level.append(cur.val) + if cur.left: + q.append(cur.left) + if cur.right: + q.append(cur.right) + res.append(level) + return res +``` + +https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof 的多语言解法👆 + +https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> zigzagLevelOrder(TreeNode* root) { + vector> res; + if (root == nullptr) { + return res; + } + + queue q; + q.push(root); + // 为 true 时向右,false 时向左 + bool flag = true; + + // while 循环控制从上向下一层层遍历 + while (!q.empty()) { + int sz = q.size(); + // 记录这一层的节点值 + deque level; + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode* cur = q.front(); + q.pop(); + // 实现 z 字形遍历 + if (flag) { + level.push_back(cur->val); + } else { + level.push_front(cur->val); + } + if (cur->left != nullptr) + q.push(cur->left); + if (cur->right != nullptr) + q.push(cur->right); + } + // 切换方向 + flag = !flag; + res.emplace_back(vector(level.begin(), level.end())); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func zigzagLevelOrder(root *TreeNode) [][]int { + res := [][]int{} + if root == nil { + return res + } + + q := []*TreeNode{root} + // 为 true 时向右,false 时向左 + flag := true + + // while 循环控制从上向下一层层遍历 + for len(q) > 0 { + sz := len(q) + // 记录这一层的节点值 + level := []int{} + // for 循环控制每一层从左向右遍历 + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + // 实现 z 字形遍历 + if flag { + level = append(level, cur.Val) + } else { + level = append([]int{cur.Val}, level...) + } + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + // 切换方向 + flag = !flag + res = append(res, level) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> zigzagLevelOrder(TreeNode root) { + List> res = new LinkedList<>(); + if (root == null) { + return res; + } + + Queue q = new LinkedList<>(); + q.offer(root); + // 为 true 时向右,false 时向左 + boolean flag = true; + + // while 循环控制从上向下一层层遍历 + while (!q.isEmpty()) { + int sz = q.size(); + // 记录这一层的节点值 + LinkedList level = new LinkedList<>(); + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + // 实现 z 字形遍历 + if (flag) { + level.addLast(cur.val); + } else { + level.addFirst(cur.val); + } + if (cur.left != null) + q.offer(cur.left); + if (cur.right != null) + q.offer(cur.right); + } + // 切换方向 + flag = !flag; + res.add(level); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var zigzagLevelOrder = function(root) { + let res = []; + if (root === null) { + return res; + } + + let q = []; + q.push(root); + // 为 true 时向右,false 时向左 + let flag = true; + + // while 循环控制从上向下一层层遍历 + while (q.length !== 0) { + let sz = q.length; + // 记录这一层的节点值 + let level = []; + // for 循环控制每一层从左向右遍历 + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + // 实现 z 字形遍历 + if (flag) { + level.push(cur.val); + } else { + level.unshift(cur.val); + } + if (cur.left !== null) + q.push(cur.left); + if (cur.right !== null) + q.push(cur.right); + } + // 切换方向 + flag = !flag; + res.push(level); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]: + res = [] + if root is None: + return res + + q = deque([root]) + # 为 True 时向右,False 时向左 + flag = True + + # while 循环控制从上向下一层层遍历 + while q: + sz = len(q) + # 记录这一层的节点值 + level = deque() + # for 循环控制每一层从左向右遍历 + for i in range(sz): + cur = q.popleft() + # 实现 z 字形遍历 + if flag: + level.append(cur.val) + else: + level.appendleft(cur.val) + if cur.left: + q.append(cur.left) + if cur.right: + q.append(cur.right) + # 切换方向 + flag = not flag + res.append(list(level)) + return res +``` + +https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof 的多语言解法👆 + +https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> levelOrder(TreeNode* root) { + vector> res; + if (root == nullptr) { + return res; + } + + queue q; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (!q.empty()) { + int sz = q.size(); + // 记录这一层的节点值 + vector level; + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode* cur = q.front(); + q.pop(); + level.push_back(cur->val); + if (cur->left != nullptr) + q.push(cur->left); + if (cur->right != nullptr) + q.push(cur->right); + } + res.push_back(level); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func levelOrder(root *TreeNode) [][]int { + res := [][]int{} + if root == nil { + return res + } + + q := []*TreeNode{} + q = append(q, root) + // while 循环控制从上向下一层层遍历 + for len(q) > 0 { + sz := len(q) + // 记录这一层的节点值 + level := []int{} + // for 循环控制每一层从左向右遍历 + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + level = append(level, cur.Val) + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + res = append(res, level) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> levelOrder(TreeNode root) { + List> res = new LinkedList<>(); + if (root == null) { + return res; + } + + Queue q = new LinkedList<>(); + q.offer(root); + // while 循环控制从上向下一层层遍历 + while (!q.isEmpty()) { + int sz = q.size(); + // 记录这一层的节点值 + List level = new LinkedList<>(); + // for 循环控制每一层从左向右遍历 + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + level.add(cur.val); + if (cur.left != null) + q.offer(cur.left); + if (cur.right != null) + q.offer(cur.right); + } + res.add(level); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var levelOrder = function(root) { + let res = []; + if (!root) { + return res; + } + + let q = []; + q.push(root); + // while 循环控制从上向下一层层遍历 + while (q.length > 0) { + let sz = q.length; + // 记录这一层的节点值 + let level = []; + // for 循环控制每一层从左向右遍历 + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + level.push(cur.val); + if (cur.left !== null) { + q.push(cur.left); + } + if (cur.right !== null) { + q.push(cur.right); + } + } + res.push(level); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def levelOrder(self, root: TreeNode) -> List[List[int]]: + res = [] + if root is None: + return res + + q = collections.deque() + q.append(root) + # while 循环控制从上向下一层层遍历 + while q: + sz = len(q) + # 记录这一层的节点值 + level = [] + # for 循环控制每一层从左向右遍历 + for i in range(sz): + cur = q.popleft() + level.append(cur.val) + if cur.left: + q.append(cur.left) + if cur.right: + q.append(cur.right) + res.append(level) + return res +``` + +https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector reversePrint(ListNode* head) { + traverse(head); + return res; + } + + // 记录链表长度 + int len = 0; + // 结果数组 + vector res; + // 结果数组中的指针 + int p = 0; + + // 递归遍历单链表 + void traverse(ListNode* head) { + if (head == nullptr) { + // 到达链表尾部,此时知道了链表的总长度 + // 创建结果数组 + res.resize(len); + return; + } + len++; + traverse(head->next); + // 后序位置,可以倒序操作链表 + res[p] = head->val; + p++; + } + + // 用「分解问题」的思路写递归解法 + // 因为 C++ 的 vector 可以支持 push_back 操作,所以我们不需要改变返回值类型 + vector reversePrint2(ListNode* head) { + // base case + if (head == nullptr) { + return {}; + } + + // 把子链表翻转的结果算出来,示例 [3,2] + vector subProblem = reversePrint2(head->next); + // 把 head 的值接到子链表的翻转结果的尾部,示例 [3,2,1] + subProblem.push_back(head->val); + return subProblem; + } +}; +``` + +```go +// by chatGPT (go) +/* +用「遍历」的思路写递归解法 +*/ + +func reversePrint(head *ListNode) []int { + var res []int + var len int + + // 递归遍历单链表 + var traverse func(*ListNode) + traverse = func(head *ListNode) { + if head == nil { + // 到达链表尾部,此时知道了链表的总长度 + // 创建结果数组 + res = make([]int, len) + return + } + len++ + traverse(head.Next) + // 后序位置,可以倒序操作链表 + res[len-p-1] = head.Val + p++ + } + + traverse(head) + return res +} + +/* +用「分解问题」的思路写递归解法 +因为 Go 不支持泛型,所以我们把返回值修改成 []int +定义:输入一个单链表,返回该链表翻转的值,示例 1->2->3 +*/ +func reversePrint2(head *ListNode) []int { + // base case + if head == nil { + return []int{} + } + + // 把子链表翻转的结果算出来,示例 [3,2] + subProblem := reversePrint2(head.Next) + // 把 head 的值接到子链表的翻转结果的尾部,示例 [3,2,1] + return append(subProblem, head.Val) +} +``` + +```java +// by labuladong (java) +// 用「遍历」的思路写递归解法 +class Solution { + public int[] reversePrint(ListNode head) { + traverse(head); + return res; + } + + // 记录链表长度 + int len = 0; + // 结果数组 + int[] res; + // 结果数组中的指针 + int p = 0; + + // 递归遍历单链表 + void traverse(ListNode head) { + if (head == null) { + // 到达链表尾部,此时知道了链表的总长度 + // 创建结果数组 + res = new int[len]; + return; + } + len++; + traverse(head.next); + // 后序位置,可以倒序操作链表 + res[p] = head.val; + p++; + } + + + + // 用「分解问题」的思路写递归解法 + // 因为 Java 的 int[] 数组不支持 add 相关的操作,所以我们把返回值修改成 List + // 定义:输入一个单链表,返回该链表翻转的值,示例 1->2->3 + List reversePrint2(ListNode head) { + // base case + if (head == null) { + return new LinkedList<>(); + } + + // 把子链表翻转的结果算出来,示例 [3,2] + List subProblem = reversePrint2(head.next); + // 把 head 的值接到子链表的翻转结果的尾部,示例 [3,2,1] + subProblem.add(head.val); + return subProblem; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 用「遍历」的思路写递归解法 + */ + +var reversePrint = function(head) { + let len = 0; // 记录链表长度 + let res = []; // 结果数组 + let p = 0; // 结果数组中的指针 + + // 递归遍历单链表 + const traverse = (head) => { + if (!head) { + // 到达链表尾部,此时知道了链表的总长度 + // 创建结果数组 + res = new Array(len); + return; + } + len++; + traverse(head.next); + // 后序位置,可以倒序操作链表 + res[p] = head.val; + p++; + }; + + traverse(head); + return res; +}; + + +/** + * 用「分解问题」的思路写递归解法 + * 因为 Java 的 int[] 数组不支持 add 相关的操作,所以我们把返回值修改成 List + * 定义:输入一个单链表,返回该链表翻转的值,示例 1->2->3 + */ +var reversePrint2 = function(head) { + // base case + if (!head) { + return new LinkedList(); + } + + // 把子链表翻转的结果算出来,示例 [3,2] + let subProblem = reversePrint2(head.next); + // 把 head 的值接到子链表的翻转结果的尾部,示例 [3,2,1] + subProblem.push(head.val); + return subProblem; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def reversePrint(self, head: ListNode) -> List[int]: + # 用「遍历」的思路写递归解法 + def traverse(head): + nonlocal len_, res, p + if not head: + res = [0] * len_ + return + len_ += 1 + traverse(head.next) + res[p] = head.val + p += 1 + + len_ = 0 + res = [] + p = 0 + traverse(head) + return res + + def reversePrint2(self, head: ListNode) -> List[int]: + # 用「分解问题」的思路写递归解法 + def sub_problem(head): + if not head: + return [] + sub_res = sub_problem(head.next) + sub_res.append(head.val) + return sub_res + + return sub_problem(head) +``` + +https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof 的多语言解法👆 + +https://leetcode.cn/problems/connecting-cities-with-minimum-cost 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minimumCost(int n, vector>& connections) { + // 城市编号为 1...n,所以初始化大小为 n + 1 + UF uf(n + 1); + // 对所有边按照权重从小到大排序 + sort(connections.begin(), connections.end(), [](vector& a, vector& b){ + return a[2] < b[2]; + }); + // 记录最小生成树的权重之和 + int mst = 0; + for (auto& edge : connections) { + int u = edge[0]; + int v = edge[1]; + int weight = edge[2]; + // 若这条边会产生环,则不能加入 mst + if (uf.connected(u, v)) { + continue; + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight; + uf.union_(u, v); + } + // 保证所有节点都被连通 + // 按理说 uf.count() == 1 说明所有节点被连通 + // 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量 + return uf.count() == 2 ? mst : -1; + } + +private: + class UF { + public: + // 连通分量个数 + int count; + // 存储一棵树 + vector parent; + // 记录树的「重量」 + vector size; + + // n 为图中节点的个数 + UF(int n) : count(n), parent(n), size(n, 1) { + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + // 将节点 p 和节点 q 连通 + void union_(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + bool connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + // 返回节点 x 的连通分量根节点 + int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + }; +}; +``` + +```go +// by chatGPT (go) +func minimumCost(n int, connections [][]int) int { + // 城市编号为 1...n,所以初始化大小为 n + 1 + uf := NewUF(n + 1) + // 对所有边按照权重从小到大排序 + sort.Slice(connections, func(i, j int) bool { + return connections[i][2] < connections[j][2] + }) + // 记录最小生成树的权重之和 + mst := 0 + for _, edge := range connections { + u := edge[0] + v := edge[1] + weight := edge[2] + // 若这条边会产生环,则不能加入 mst + if uf.connected(u, v) { + continue + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight + uf.union(u, v) + } + // 保证所有节点都被连通 + // 按理说 uf.count() == 1 说明所有节点被连通 + // 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量 + if uf.count() == 2 { + return mst + } + return -1 +} + +type UF struct { + // 连通分量个数 + count int + // 存储一棵树 + parent []int + // 记录树的「重量」 + size []int +} + +// n 为图中节点的个数 +func NewUF(n int) *UF { + uf := &UF{} + uf.count = n + uf.parent = make([]int, n) + uf.size = make([]int, n) + for i := 0; i < n; i++ { + uf.parent[i] = i + uf.size[i] = 1 + } + return uf +} + +// 将节点 p 和节点 q 连通 +func (uf *UF) union(p, q int) { + rootP := uf.find(p) + rootQ := uf.find(q) + if rootP == rootQ { + return + } + + // 小树接到大树下面,较平衡 + if uf.size[rootP] > uf.size[rootQ] { + uf.parent[rootQ] = rootP + uf.size[rootP] += uf.size[rootQ] + } else { + uf.parent[rootP] = rootQ + uf.size[rootQ] += uf.size[rootP] + } + // 两个连通分量合并成一个连通分量 + uf.count-- +} + +// 判断节点 p 和节点 q 是否连通 +func (uf *UF) connected(p, q int) bool { + rootP := uf.find(p) + rootQ := uf.find(q) + return rootP == rootQ +} + +// 返回节点 x 的连通分量根节点 +func (uf *UF) find(x int) int { + for uf.parent[x] != x { + // 进行路径压缩 + uf.parent[x] = uf.parent[uf.parent[x]] + x = uf.parent[x] + } + return x +} + +// 返回图中的连通分量个数 +func (uf *UF) count() int { + return uf.count +} +``` + +```java +// by labuladong (java) +class Solution { + public int minimumCost(int n, int[][] connections) { + // 城市编号为 1...n,所以初始化大小为 n + 1 + UF uf = new UF(n + 1); + // 对所有边按照权重从小到大排序 + Arrays.sort(connections, (a, b) -> (a[2] - b[2])); + // 记录最小生成树的权重之和 + int mst = 0; + for (int[] edge : connections) { + int u = edge[0]; + int v = edge[1]; + int weight = edge[2]; + // 若这条边会产生环,则不能加入 mst + if (uf.connected(u, v)) { + continue; + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight; + uf.union(u, v); + } + // 保证所有节点都被连通 + // 按理说 uf.count() == 1 说明所有节点被连通 + // 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量 + return uf.count() == 2 ? mst : -1; + } + + class UF { + // 连通分量个数 + private int count; + // 存储一棵树 + private int[] parent; + // 记录树的「重量」 + private int[] size; + + // n 为图中节点的个数 + public UF(int n) { + this.count = n; + parent = new int[n]; + size = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + // 将节点 p 和节点 q 连通 + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + // 返回节点 x 的连通分量根节点 + private int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + public int count() { + return count; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @param {number[][]} connections + * @return {number} + */ +var minimumCost = function(n, connections) { + // 城市编号为 1...n,所以初始化大小为 n + 1 + const uf = new UF(n + 1); + // 对所有边按照权重从小到大排序 + connections.sort((a, b) => a[2] - b[2]); + // 记录最小生成树的权重之和 + let mst = 0; + for (const edge of connections) { + const u = edge[0]; + const v = edge[1]; + const weight = edge[2]; + // 若这条边会产生环,则不能加入 mst + if (uf.connected(u, v)) { + continue; + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight; + uf.union(u, v); + } + // 保证所有节点都被连通 + // 按理说 uf.count() == 1 说明所有节点被连通 + // 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量 + return uf.count() == 2 ? mst : -1; +}; + +class UF { + // 连通分量个数 + #count; + // 存储一棵树 + #parent; + // 记录树的「重量」 + #size; + + // n 为图中节点的个数 + constructor(n) { + this.#count = n; + this.#parent = new Array(n); + this.#size = new Array(n); + for (let i = 0; i < n; i++) { + this.#parent[i] = i; + this.#size[i] = 1; + } + } + + // 将节点 p 和节点 q 连通 + union(p, q) { + const rootP = this.find(p); + const rootQ = this.find(q); + if (rootP === rootQ) + return; + + // 小树接到大树下面,较平衡 + if (this.#size[rootP] > this.#size[rootQ]) { + this.#parent[rootQ] = rootP; + this.#size[rootP] += this.#size[rootQ]; + } else { + this.#parent[rootP] = rootQ; + this.#size[rootQ] += this.#size[rootP]; + } + // 两个连通分量合并成一个连通分量 + this.#count--; + } + + // 判断节点 p 和节点 q 是否连通 + connected(p, q) { + const rootP = this.find(p); + const rootQ = this.find(q); + return rootP === rootQ; + } + + // 返回节点 x 的连通分量根节点 + find(x) { + while (this.#parent[x] !== x) { + // 进行路径压缩 + this.#parent[x] = this.#parent[this.#parent[x]]; + x = this.#parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + count() { + return this.#count; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def minimumCost(self, n: int, connections: List[List[int]]) -> int: + # 城市编号为 1...n,所以初始化大小为 n + 1 + uf = UF(n + 1) + # 对所有边按照权重从小到大排序 + connections.sort(key=lambda x: x[2]) + # 记录最小生成树的权重之和 + mst = 0 + for edge in connections: + u, v, weight = edge + # 若这条边会产生环,则不能加入 mst + if uf.connected(u, v): + continue + # 若这条边不会产生环,则属于最小生成树 + mst += weight + uf.union(u, v) + # 保证所有节点都被连通 + # 按理说 uf.count() == 1 说明所有节点被连通 + # 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量 + return mst if uf.count() == 2 else -1 + +class UF: + # 连通分量个数 + def __init__(self, n: int): + self.count = n + # 存储一棵树 + self.parent = list(range(n)) + # 记录树的「重量」 + self.size = [1] * n + + # 将节点 p 和节点 q 连通 + def union(self, p: int, q: int): + rootP = self.find(p) + rootQ = self.find(q) + if rootP == rootQ: + return + + # 小树接到大树下面,较平衡 + if self.size[rootP] > self.size[rootQ]: + self.parent[rootQ] = rootP + self.size[rootP] += self.size[rootQ] + else: + self.parent[rootP] = rootQ + self.size[rootQ] += self.size[rootP] + # 两个连通分量合并成一个连通分量 + self.count -= 1 + + # 判断节点 p 和节点 q 是否连通 + def connected(self, p: int, q: int) -> bool: + rootP = self.find(p) + rootQ = self.find(q) + return rootP == rootQ + + # 返回节点 x 的连通分量根节点 + def find(self, x: int) -> int: + while self.parent[x] != x: + # 进行路径压缩 + self.parent[x] = self.parent[self.parent[x]] + x = self.parent[x] + return x + + # 返回图中的连通分量个数 + def count(self) -> int: + return self.count +``` + +https://leetcode.cn/problems/connecting-cities-with-minimum-cost 的多语言解法👆 + +https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + // 存储 inorder 中值到索引的映射 + unordered_map valToIndex; + +public: + TreeNode* buildTree(vector& inorder, vector& postorder) { + for (int i = 0; i < inorder.size(); i++) { + valToIndex[inorder[i]] = i; + } + return build(inorder, 0, inorder.size() - 1, + postorder, 0, postorder.size() - 1); + } + + /* + 定义: + 中序遍历数组为 inorder[inStart..inEnd], + 后序遍历数组为 postorder[postStart..postEnd], + 构造这个二叉树并返回该二叉树的根节点 + */ + TreeNode* build(vector& inorder, int inStart, int inEnd, + vector& postorder, int postStart, int postEnd) { + + if (inStart > inEnd) { + return nullptr; + } + // root 节点对应的值就是后序遍历数组的最后一个元素 + int rootVal = postorder[postEnd]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex[rootVal]; + // 左子树的节点个数 + int leftSize = index - inStart; + TreeNode* root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/6.jpeg) + */ + // 递归构造左右子树 + root->left = build(inorder, inStart, index - 1, + postorder, postStart, postStart + leftSize - 1); + + root->right = build(inorder, index + 1, inEnd, + postorder, postStart + leftSize, postEnd - 1); + return root; + } +}; +``` + +```go +// by chatGPT (go) +func buildTree(inorder []int, postorder []int) *TreeNode { + // 存储 inorder 中值到索引的映射 + valToIndex := make(map[int]int) + for i, val := range inorder { + valToIndex[val] = i + } + return build(inorder, 0, len(inorder)-1, + postorder, 0, len(postorder)-1, valToIndex) +} + +/* + 定义: + 中序遍历数组为 inorder[inStart..inEnd], + 后序遍历数组为 postorder[postStart..postEnd], + 构造这个二叉树并返回该二叉树的根节点 +*/ +func build(inorder []int, inStart int, inEnd int, + postorder []int, postStart int, postEnd int, + valToIndex map[int]int) *TreeNode { + if inStart > inEnd { + return nil + } + // root 节点对应的值就是后序遍历数组的最后一个元素 + rootVal := postorder[postEnd] + // rootVal 在中序遍历数组中的索引 + index := valToIndex[rootVal] + // 左子树的节点个数 + leftSize := index - inStart + root := &TreeNode{Val: rootVal} + // 递归构造左右子树 + root.Left = build(inorder, inStart, index-1, + postorder, postStart, postStart+leftSize-1, valToIndex) + root.Right = build(inorder, index+1, inEnd, + postorder, postStart+leftSize, postEnd-1, valToIndex) + return root +} +``` + +```java +// by labuladong (java) +class Solution { + // 存储 inorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode buildTree(int[] inorder, int[] postorder) { + for (int i = 0; i < inorder.length; i++) { + valToIndex.put(inorder[i], i); + } + return build(inorder, 0, inorder.length - 1, + postorder, 0, postorder.length - 1); + } + + /* + 定义: + 中序遍历数组为 inorder[inStart..inEnd], + 后序遍历数组为 postorder[postStart..postEnd], + 构造这个二叉树并返回该二叉树的根节点 + */ + TreeNode build(int[] inorder, int inStart, int inEnd, + int[] postorder, int postStart, int postEnd) { + + if (inStart > inEnd) { + return null; + } + // root 节点对应的值就是后序遍历数组的最后一个元素 + int rootVal = postorder[postEnd]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex.get(rootVal); + // 左子树的节点个数 + int leftSize = index - inStart; + TreeNode root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/6.jpeg) + */ + // 递归构造左右子树 + root.left = build(inorder, inStart, index - 1, + postorder, postStart, postStart + leftSize - 1); + + root.right = build(inorder, index + 1, inEnd, + postorder, postStart + leftSize, postEnd - 1); + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} inorder + * @param {number[]} postorder + * @return {TreeNode} + */ +var buildTree = function(inorder, postorder) { + // 存储 inorder 中值到索引的映射 + const valToIndex = new Map(); + for (let i = 0; i < inorder.length; i++) { + valToIndex.set(inorder[i], i); + } + return build(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1, valToIndex); +}; + +/** +* 定义: +* 中序遍历数组为 inorder[inStart..inEnd], +* 后序遍历数组为 postorder[postStart..postEnd], +* 构造这个二叉树并返回该二叉树的根节点 +*/ +function build(inorder, inStart, inEnd, postorder, postStart, postEnd, valToIndex) { + if (inStart > inEnd) { + return null; + } + // root 节点对应的值就是后序遍历数组的最后一个元素 + const rootVal = postorder[postEnd]; + // rootVal 在中序遍历数组中的索引 + const index = valToIndex.get(rootVal); + // 左子树的节点个数 + const leftSize = index - inStart; + const root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/6.jpeg) + */ + // 递归构造左右子树 + root.left = build(inorder, inStart, index - 1, postorder, postStart, postStart + leftSize - 1, valToIndex); + root.right = build(inorder, index + 1, inEnd, postorder, postStart + leftSize, postEnd - 1, valToIndex); + return root; +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + # 存储 inorder 中值到索引的映射 + self.valToIndex = {} + + def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode: + for i in range(len(inorder)): + self.valToIndex[inorder[i]] = i + return self.build(inorder, 0, len(inorder) - 1, + postorder, 0, len(postorder) - 1) + + ''' + 定义: + 中序遍历数组为 inorder[inStart..inEnd], + 后序遍历数组为 postorder[postStart..postEnd], + 构造这个二叉树并返回该二叉树的根节点 + ''' + def build(self, inorder: List[int], inStart: int, inEnd: int, + postorder: List[int], postStart: int, postEnd: int) -> TreeNode: + + if inStart > inEnd: + return None + # root 节点对应的值就是后序遍历数组的最后一个元素 + rootVal = postorder[postEnd] + # rootVal 在中序遍历数组中的索引 + index = self.valToIndex[rootVal] + # 左子树的节点个数 + leftSize = index - inStart + root = TreeNode(rootVal) + ''' + + ![](../pictures/二叉树系列2/6.jpeg) + ''' + # 递归构造左右子树 + root.left = self.build(inorder, inStart, index - 1, + postorder, postStart, postStart + leftSize - 1) + + root.right = self.build(inorder, index + 1, inEnd, + postorder, postStart + leftSize, postEnd - 1) + return root +``` + +https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + // 存储 inorder 中值到索引的映射 + unordered_map valToIndex; + +public: + TreeNode* buildTree(vector& preorder, vector& inorder) { + for (int i = 0; i < inorder.size(); i++) { + valToIndex[inorder[i]] = i; + } + return build(preorder, 0, preorder.size() - 1, + inorder, 0, inorder.size() - 1); + } + + /* + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 + */ + TreeNode* build(vector& preorder, int preStart, int preEnd, + vector& inorder, int inStart, int inEnd) { + if (preStart > preEnd) { + return nullptr; + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex[rootVal]; + + int leftSize = index - inStart; + + // 先构造出当前根节点 + TreeNode* root = new TreeNode(rootVal); + // 递归构造左右子树 + root->left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + root->right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); + + return root; + } +}; +``` + +```go +// by chatGPT (go) +func buildTree(preorder []int, inorder []int) *TreeNode { + // 存储 inorder 中值到索引的映射 + valToIndex := make(map[int]int) + for i, v := range inorder { + valToIndex[v] = i + } + return build(preorder, 0, len(preorder)-1, + inorder, 0, len(inorder)-1, valToIndex) +} + +/* + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 +*/ +func build(preorder []int, preStart int, preEnd int, + inorder []int, inStart int, inEnd int, + valToIndex map[int]int) *TreeNode { + if preStart > preEnd { + return nil + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + rootVal := preorder[preStart] + // rootVal 在中序遍历数组中的索引 + index := valToIndex[rootVal] + + leftSize := index - inStart + + // 先构造出当前根节点 + root := &TreeNode{Val: rootVal} + // 递归构造左右子树 + root.Left = build(preorder, preStart+1, preStart+leftSize, + inorder, inStart, index-1, valToIndex) + + root.Right = build(preorder, preStart+leftSize+1, preEnd, + inorder, index+1, inEnd, valToIndex) + return root +} +``` + +```java +// by labuladong (java) +class Solution { + // 存储 inorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode buildTree(int[] preorder, int[] inorder) { + for (int i = 0; i < inorder.length; i++) { + valToIndex.put(inorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); + } + + /* + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 + */ + TreeNode build(int[] preorder, int preStart, int preEnd, + int[] inorder, int inStart, int inEnd) { + if (preStart > preEnd) { + return null; + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex.get(rootVal); + + int leftSize = index - inStart; + + // 先构造出当前根节点 + TreeNode root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/4.jpeg) + */ + // 递归构造左右子树 + root.left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + + root.right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +var buildTree = function(preorder, inorder) { + // 存储 inorder 中值到索引的映射 + var valToIndex = new Map(); + for (var i = 0; i < inorder.length; i++) { + valToIndex.set(inorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); +}; + +/* + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 +*/ +var build = function(preorder, preStart, preEnd, + inorder, inStart, inEnd) { + if (preStart > preEnd) { + return null; + } + // root 节点对应的值就是前序遍历数组的第一个元素 + var rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + var index = valToIndex.get(rootVal); + var leftSize = index - inStart; + // 先构造出当前根节点 + var root = new TreeNode(rootVal); + // 递归构造左右子树 + root.left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + root.right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); + return root; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: + # 存储 inorder 中值到索引的映射 + valToIndex = {} + for i in range(len(inorder)): + valToIndex[inorder[i]] = i + + return self.build(preorder, 0, len(preorder) - 1, + inorder, 0, len(inorder) - 1, valToIndex) + + def build(self, preorder, preStart, preEnd, inorder, inStart, inEnd, valToIndex): + if preStart > preEnd: + return None + + # root 节点对应的值就是前序遍历数组的第一个元素 + rootVal = preorder[preStart] + # rootVal 在中序遍历数组中的索引 + index = valToIndex[rootVal] + + leftSize = index - inStart + + # 先构造出当前根节点 + root = TreeNode(rootVal) + + # 递归构造左右子树 + root.left = self.build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1, valToIndex) + + root.right = self.build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd, valToIndex) + return root +``` + +https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 存储 postorder 中值到索引的映射 + unordered_map valToIndex; + +public: + TreeNode* constructFromPrePost(vector& preorder, vector& postorder) { + for (int i = 0; i < postorder.size(); i++) { + valToIndex[postorder[i]] = i; + } + return build(preorder, 0, preorder.size() - 1, + postorder, 0, postorder.size() - 1); + } + + // 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd] + // 构建二叉树,并返回根节点。 + TreeNode* build(vector& preorder, int preStart, int preEnd, + vector& postorder, int postStart, int postEnd) { + if (preStart > preEnd) { + return nullptr; + } + if (preStart == preEnd) { + return new TreeNode(preorder[preStart]); + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // root.left 的值是前序遍历第二个元素 + // 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点 + // 确定 preorder 和 postorder 中左右子树的元素区间 + int leftRootVal = preorder[preStart + 1]; + // leftRootVal 在后序遍历数组中的索引 + int index = valToIndex[leftRootVal]; + // 左子树的元素个数 + int leftSize = index - postStart + 1; + + // 先构造出当前根节点 + TreeNode* root = new TreeNode(rootVal); + + // 递归构造左右子树 + // 根据左子树的根节点索引和元素个数推导左右子树的索引边界 + root->left = build(preorder, preStart + 1, preStart + leftSize, + postorder, postStart, index); + root->right = build(preorder, preStart + leftSize + 1, preEnd, + postorder, index + 1, postEnd - 1); + + return root; + } +}; +``` + +```go +// by chatGPT (go) +// Definition for a binary tree node. +// type TreeNode struct { +// Val int +// Left *TreeNode +// Right *TreeNode +// } + +func constructFromPrePost(preorder []int, postorder []int) *TreeNode { + // 存储 postorder 中值到索引的映射 + valToIndex := make(map[int]int) + for i, v := range postorder { + valToIndex[v] = i + } + return build(preorder, 0, len(preorder)-1, postorder, 0, len(postorder)-1, valToIndex) +} + +// 根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd] 构建二叉树,并返回根节点。 +func build(preorder []int, preStart int, preEnd int, postorder []int, postStart int, postEnd int, valToIndex map[int]int) *TreeNode { + if preStart > preEnd { + return nil + } + if preStart == preEnd { + return &TreeNode{Val: preorder[preStart]} + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + rootVal := preorder[preStart] + // root.left 的值是前序遍历第二个元素 + // 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点 + // 确定 preorder 和 postorder 中左右子树的元素区间 + leftRootVal := preorder[preStart+1] + // leftRootVal 在后序遍历数组中的索引 + index := valToIndex[leftRootVal] + // 左子树的元素个数 + leftSize := index - postStart + 1 + + // 先构造出当前根节点 + root := &TreeNode{Val: rootVal} + // 递归构造左右子树 + // 根据左子树的根节点索引和元素个数推导左右子树的索引边界 + root.Left = build(preorder, preStart+1, preStart+leftSize, postorder, postStart, index, valToIndex) + root.Right = build(preorder, preStart+leftSize+1, preEnd, postorder, index+1, postEnd-1, valToIndex) + + return root +} +``` + +```java +// by labuladong (java) +class Solution { + // 存储 postorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode constructFromPrePost(int[] preorder, int[] postorder) { + for (int i = 0; i < postorder.length; i++) { + valToIndex.put(postorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + postorder, 0, postorder.length - 1); + } + + // 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd] + // 构建二叉树,并返回根节点。 + TreeNode build(int[] preorder, int preStart, int preEnd, + int[] postorder, int postStart, int postEnd) { + if (preStart > preEnd) { + return null; + } + if (preStart == preEnd) { + return new TreeNode(preorder[preStart]); + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // root.left 的值是前序遍历第二个元素 + // 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点 + // 确定 preorder 和 postorder 中左右子树的元素区间 + int leftRootVal = preorder[preStart + 1]; + // leftRootVal 在后序遍历数组中的索引 + int index = valToIndex.get(leftRootVal); + // 左子树的元素个数 + int leftSize = index - postStart + 1; + + // 先构造出当前根节点 + TreeNode root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/8.jpeg) + */ + // 递归构造左右子树 + // 根据左子树的根节点索引和元素个数推导左右子树的索引边界 + root.left = build(preorder, preStart + 1, preStart + leftSize, + postorder, postStart, index); + root.right = build(preorder, preStart + leftSize + 1, preEnd, + postorder, index + 1, postEnd - 1); + + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} preorder + * @param {number[]} postorder + * @return {TreeNode} + */ +var constructFromPrePost = function(preorder, postorder) { + // 存储 postorder 中值到索引的映射 + const valToIndex = new Map(); + for (let i = 0; i < postorder.length; i++) { + valToIndex.set(postorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + postorder, 0, postorder.length - 1, valToIndex); +}; + +/** + * @param {number[]} preorder + * @param {number} preStart + * @param {number} preEnd + * @param {number[]} postorder + * @param {number} postStart + * @param {number} postEnd + * @param {Map} valToIndex + * @return {TreeNode} + */ +function build(preorder, preStart, preEnd, postorder, postStart, postEnd, valToIndex) { + if (preStart > preEnd) { + return null; + } + if (preStart === preEnd) { + return new TreeNode(preorder[preStart]); + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + const rootVal = preorder[preStart]; + // root.left 的值是前序遍历第二个元素 + // 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点 + // 确定 preorder 和 postorder 中左右子树的元素区间 + const leftRootVal = preorder[preStart + 1]; + // leftRootVal 在后序遍历数组中的索引 + const index = valToIndex.get(leftRootVal); + // 左子树的元素个数 + const leftSize = index - postStart + 1; + + // 先构造出当前根节点 + const root = new TreeNode(rootVal); + + // 递归构造左右子树 + // 根据左子树的根节点索引和元素个数推导左右子树的索引边界 + root.left = build(preorder, preStart + 1, preStart + leftSize, + postorder, postStart, index, valToIndex); + root.right = build(preorder, preStart + leftSize + 1, preEnd, + postorder, index + 1, postEnd - 1, valToIndex); + + return root; +} +``` + +```python +# by chatGPT (python) +class Solution: + # 存储 postorder 中值到索引的映射 + valToIndex = {} + + def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> TreeNode: + for i in range(len(postorder)): + self.valToIndex[postorder[i]] = i + return self.build(preorder, 0, len(preorder) - 1, + postorder, 0, len(postorder) - 1) + + # 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd] + # 构建二叉树,并返回根节点。 + def build(self, preorder, preStart, preEnd, postorder, postStart, postEnd): + if preStart > preEnd: + return None + if preStart == preEnd: + return TreeNode(preorder[preStart]) + + # root 节点对应的值就是前序遍历数组的第一个元素 + rootVal = preorder[preStart] + # root.left 的值是前序遍历第二个元素 + # 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点 + # 确定 preorder 和 postorder 中左右子树的元素区间 + leftRootVal = preorder[preStart + 1] + # leftRootVal 在后序遍历数组中的索引 + index = self.valToIndex[leftRootVal] + # 左子树的元素个数 + leftSize = index - postStart + 1 + + # 先构造出当前根节点 + root = TreeNode(rootVal) + # 递归构造左右子树 + # 根据左子树的根节点索引和元素个数推导左右子树的索引边界 + root.left = self.build(preorder, preStart + 1, preStart + leftSize, + postorder, postStart, index) + root.right = self.build(preorder, preStart + leftSize + 1, preEnd, + postorder, index + 1, postEnd - 1) + + return root +``` + +https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/container-with-most-water 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxArea(vector& height) { + int left = 0, right = height.size() - 1; + int res = 0; + while (left < right) { + // [left, right] 之间的矩形面积 + int cur_area = min(height[left], height[right]) * (right - left); + res = max(res, cur_area); + // 双指针技巧,移动较低的一边 + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func maxArea(height []int) int { + left, right := 0, len(height)-1 + res := 0 + for left < right { + // [left, right] 之间的矩形面积 + curArea := func() int { + if height[left] < height[right] { + return height[left] * (right - left) + } + return height[right] * (right - left) + }() + res = func() int { + if curArea > res { + return curArea + } + return res + }() + // 双指针技巧,移动较低的一边 + if height[left] < height[right] { + left++ + } else { + right-- + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxArea(int[] height) { + int left = 0, right = height.length - 1; + int res = 0; + while (left < right) { + // [left, right] 之间的矩形面积 + int cur_area = Math.min(height[left], height[right]) * (right - left); + res = Math.max(res, cur_area); + // 双指针技巧,移动较低的一边 + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} height + * @return {number} + */ +var maxArea = function(height) { + let left = 0, right = height.length - 1; + let res = 0; + while (left < right) { + // [left, right] 之间的矩形面积 + const cur_area = Math.min(height[left], height[right]) * (right - left); + res = Math.max(res, cur_area); + // 双指针技巧,移动较低的一边 + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxArea(self, height: List[int]) -> int: + left, right = 0, len(height)-1 + res = 0 + while left < right: + # [left, right] 之间的矩形面积 + cur_area = min(height[left], height[right]) * (right - left) + res = max(res, cur_area) + # 双指针技巧,移动较低的一边 + if height[left] < height[right]: + left += 1 + else: + right -= 1 + return res +``` + +https://leetcode.cn/problems/container-with-most-water 的多语言解法👆 + +https://leetcode.cn/problems/convert-bst-to-greater-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + TreeNode* convertBST(TreeNode* root) { + traverse(root); + return root; + } + + // 记录累加和 + int sum = 0; + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + traverse(root->right); + // 维护累加和 + sum += root->val; + // 将 BST 转化成累加树 + root->val = sum; + traverse(root->left); + } +}; +``` + +```go +// by chatGPT (go) +func convertBST(root *TreeNode) *TreeNode { + // 记录累加和 + var sum int + var traverse func(node *TreeNode) + traverse = func(node *TreeNode) { + if node == nil { + return + } + traverse(node.Right) + // 维护累加和 + sum += node.Val + // 将 BST 转化成累加树 + node.Val = sum + traverse(node.Left) + } + traverse(root) + return root +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode convertBST(TreeNode root) { + traverse(root); + return root; + } + + // 记录累加和 + int sum = 0; + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.right); + // 维护累加和 + sum += root.val; + // 将 BST 转化成累加树 + root.val = sum; + traverse(root.left); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {TreeNode} + */ +var convertBST = function(root) { + // 记录累加和 + let sum = 0; + // 中序遍历节点 + const traverse = function(node) { + if (!node) { + return; + } + traverse(node.right); // 先遍历右子树 + sum += node.val; // 维护累加和 + node.val = sum; // 将 BST 节点的值更新为累加和 + traverse(node.left); // 遍历左子树 + } + traverse(root); + return root; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def convertBST(self, root: TreeNode) -> TreeNode: + self.sum = 0 + self.traverse(root) + return root + + def traverse(self, root: TreeNode) -> None: + if not root: + return + self.traverse(root.right) + # 维护累加和 + self.sum += root.val + # 将 BST 转化成累加树 + root.val = self.sum + self.traverse(root.left) +``` + +https://leetcode.cn/problems/convert-bst-to-greater-tree 的多语言解法👆 + +https://leetcode.cn/problems/copy-list-with-random-pointer 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + Node* copyRandomList(Node* head) { + unordered_map originToClone; + // 第一次遍历,先把所有节点克隆出来 + for (Node* p = head; p != nullptr; p = p->next) { + if (originToClone.count(p) == 0) { + originToClone[p] = new Node(p->val); + } + } + // 第二次遍历,把克隆节点的结构连接好 + for (Node* p = head; p != nullptr; p = p->next) { + if (p->next != nullptr) { + originToClone[p]->next = originToClone[p->next]; + } + if (p->random != nullptr) { + originToClone[p]->random = originToClone[p->random]; + } + } + // 返回克隆之后的头结点 + return originToClone[head]; + } +}; + +// 用递归的方式进行遍历 +class Solution2 { +public: + Node* copyRandomList(Node* head) { + traverse(head); + return originToClone[head]; + } + +private: + // 记录 DFS 遍历过的节点,防止走回头路 + unordered_set visited; + // 记录原节点到克隆节点的映射 + unordered_map originToClone; + + // DFS 图遍历框架 + void traverse(Node* node) { + if (node == nullptr) { + return; + } + if (visited.count(node) > 0) { + return; + } + // 前序位置,标记为已访问 + visited.insert(node); + // 前序位置,克隆节点 + if (originToClone.count(node) == 0) { + originToClone[node] = new Node(node->val); + } + Node* cloneNode = originToClone[node]; + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + traverse(node->next); + cloneNode->next = originToClone[node->next]; + + traverse(node->random); + cloneNode->random = originToClone[node->random]; + } +}; +``` + +```go +// by chatGPT (go) +func copyRandomList(head *Node) *Node { + originToClone := make(map[*Node]*Node) + // 第一次遍历,先把所有节点克隆出来 + for p := head; p != nil; p = p.Next { + if _, ok := originToClone[p]; !ok { + originToClone[p] = &Node{ + Val: p.Val, + } + } + } + // 第二次遍历,把克隆节点的结构连接好 + for p := head; p != nil; p = p.Next { + if p.Next != nil { + originToClone[p].Next = originToClone[p.Next] + } + if p.Random != nil { + originToClone[p].Random = originToClone[p.Random] + } + } + // 返回克隆之后的头结点 + return originToClone[head] +} + +// 用递归的方式进行遍历 +func copyRandomList2(head *Node) *Node { + visited := make(map[*Node]bool) + originToClone := make(map[*Node]*Node) + traverse(head, visited, originToClone) + return originToClone[head] +} + +// DFS 图遍历框架 +func traverse(node *Node, visited map[*Node]bool, originToClone map[*Node]*Node) { + if node == nil { + return + } + if visited[node] { + return + } + // 前序位置,标记为已访问 + visited[node] = true + // 前序位置,克隆节点 + if _, ok := originToClone[node]; !ok { + originToClone[node] = &Node{ + Val: node.Val, + } + } + cloneNode := originToClone[node] + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + + traverse(node.Next, visited, originToClone) + cloneNode.Next = originToClone[node.Next] + + traverse(node.Random, visited, originToClone) + cloneNode.Random = originToClone[node.Random] +} +``` + +```java +// by labuladong (java) +class Solution { + public Node copyRandomList(Node head) { + HashMap originToClone = new HashMap<>(); + // 第一次遍历,先把所有节点克隆出来 + for (Node p = head; p != null; p = p.next) { + if (!originToClone.containsKey(p)) { + originToClone.put(p, new Node(p.val)); + } + } + // 第二次遍历,把克隆节点的结构连接好 + for (Node p = head; p != null; p = p.next) { + if (p.next != null) { + originToClone.get(p).next = originToClone.get(p.next); + } + if (p.random != null) { + originToClone.get(p).random = originToClone.get(p.random); + } + } + // 返回克隆之后的头结点 + return originToClone.get(head); + } +} + +// 用递归的方式进行遍历 +class Solution2 { + public Node copyRandomList(Node head) { + traverse(head); + return originToClone.get(head); + } + + // 记录 DFS 遍历过的节点,防止走回头路 + HashSet visited = new HashSet<>(); + // 记录原节点到克隆节点的映射 + HashMap originToClone = new HashMap<>(); + + // DFS 图遍历框架 + void traverse(Node node) { + if (node == null) { + return; + } + if (visited.contains(node)) { + return; + } + // 前序位置,标记为已访问 + visited.add(node); + // 前序位置,克隆节点 + if (!originToClone.containsKey(node)) { + originToClone.put(node, new Node(node.val)); + } + Node cloneNode = originToClone.get(node); + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + + traverse(node.next); + cloneNode.next = originToClone.get(node.next); + + traverse(node.random); + cloneNode.random = originToClone.get(node.random); + } +} +``` + +```javascript +// by chatGPT (javascript) +var copyRandomList = function(head) { + const originToClone = new Map(); + + // 第一次遍历,先把所有节点克隆出来 + for (let p = head; p !== null; p = p.next) { + if (!originToClone.has(p)) { + originToClone.set(p, new Node(p.val)); + } + } + + // 第二次遍历,把克隆节点的结构连接好 + for (let p = head; p !== null; p = p.next) { + if (p.next !== null) { + originToClone.get(p).next = originToClone.get(p.next); + } + if (p.random !== null) { + originToClone.get(p).random = originToClone.get(p.random); + } + } + + // 返回克隆之后的头结点 + return originToClone.get(head); +}; + +// 用递归的方式进行遍历 +var copyRandomList2 = function(head) { + const visited = new Set(); + const originToClone = new Map(); + + const traverse = (node) => { + if (node === null) { + return; + } + if (visited.has(node)) { + return; + } + // 前序位置,标记为已访问 + visited.add(node); + // 前序位置,克隆节点 + if (!originToClone.has(node)) { + originToClone.set(node, new Node(node.val)); + } + const cloneNode = originToClone.get(node); + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + traverse(node.next); + cloneNode.next = originToClone.get(node.next); + + traverse(node.random); + cloneNode.random = originToClone.get(node.random); + }; + + traverse(head); + return originToClone.get(head); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def copyRandomList(self, head: 'Node') -> 'Node': + originToClone = {} + # 第一次遍历,先把所有节点克隆出来 + p = head + while p: + if p not in originToClone: + originToClone[p] = Node(p.val) + p = p.next + # 第二次遍历,把克隆节点的结构连接好 + p = head + while p: + if p.next: + originToClone[p].next = originToClone[p.next] + if p.random: + originToClone[p].random = originToClone[p.random] + p = p.next + # 返回克隆之后的头结点 + return originToClone.get(head) + +# 用递归的方式进行遍历 +class Solution2: + def copyRandomList(self, head: 'Node') -> 'Node': + self.visited = set() + self.originToClone = {} + self.traverse(head) + return self.originToClone.get(head) + + # DFS 图遍历框架 + def traverse(self, node): + if not node: + return + if node in self.visited: + return + # 前序位置,标记为已访问 + self.visited.add(node) + # 前序位置,克隆节点 + if node not in self.originToClone: + self.originToClone[node] = Node(node.val) + cloneNode = self.originToClone[node] + + # 递归遍历邻居节点,并构建克隆图 + # 递归之后,邻居节点一定存在 originToClone 中 + self.traverse(node.next) + cloneNode.next = self.originToClone.get(node.next) + + self.traverse(node.random) + cloneNode.random = self.originToClone.get(node.random) +``` + +https://leetcode.cn/problems/copy-list-with-random-pointer 的多语言解法👆 + +https://leetcode.cn/problems/corporate-flight-bookings 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector corpFlightBookings(vector>& bookings, int n) { + // nums 初始化为全 0 + vector nums(n); + // 构造差分解法 + Difference df(nums); + + for (auto& booking : bookings) { + // 注意转成数组索引要减一哦 + int i = booking[0] - 1; + int j = booking[1] - 1; + int val = booking[2]; + // 对区间 nums[i..j] 增加 val + df.increment(i, j, val); + } + // 返回最终的结果数组 + return df.result(); + } + +private: + class Difference { + // 差分数组 + vector diff; + + public: + Difference(vector& nums) { + assert(nums.size() > 0); + diff = vector(nums.size()); + // 构造差分数组 + diff[0] = nums[0]; + for (int i = 1; i < nums.size(); i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.size()) { + diff[j + 1] -= val; + } + } + + vector result() { + vector res(diff.size()); + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (int i = 1; i < diff.size(); i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } + }; +}; +``` + +```go +// by chatGPT (go) +func corpFlightBookings(bookings [][]int, n int) []int { + // nums 初始化为全 0 + nums := make([]int, n) + // 构造差分解法 + df := Difference{nums} + + for _, booking := range bookings { + // 注意转成数组索引要减一哦 + i := booking[0] - 1 + j := booking[1] - 1 + val := booking[2] + // 对区间 nums[i..j] 增加 val + df.increment(i, j, val) + } + // 返回最终的结果数组 + return df.result() +} + +type Difference struct { + // 差分数组 + diff []int +} + +func (df *Difference) increment(i, j, val int) { + df.diff[i] += val + if j+1 < len(df.diff) { + df.diff[j+1] -= val + } +} + +func (df *Difference) result() []int { + res := make([]int, len(df.diff)) + // 根据差分数组构造结果数组 + res[0] = df.diff[0] + for i := 1; i < len(df.diff); i++ { + res[i] = res[i-1] + df.diff[i] + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] corpFlightBookings(int[][] bookings, int n) { + // nums 初始化为全 0 + int[] nums = new int[n]; + // 构造差分解法 + Difference df = new Difference(nums); + + for (int[] booking : bookings) { + // 注意转成数组索引要减一哦 + int i = booking[0] - 1; + int j = booking[1] - 1; + int val = booking[2]; + // 对区间 nums[i..j] 增加 val + df.increment(i, j, val); + } + // 返回最终的结果数组 + return df.result(); + } + + class Difference { + // 差分数组 + private int[] diff; + + public Difference(int[] nums) { + assert nums.length > 0; + diff = new int[nums.length]; + // 构造差分数组 + diff[0] = nums[0]; + for (int i = 1; i < nums.length; i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + public void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.length) { + diff[j + 1] -= val; + } + } + + public int[] result() { + int[] res = new int[diff.length]; + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (int i = 1; i < diff.length; i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } + } + +} +``` + +```javascript +// by chatGPT (javascript) +var corpFlightBookings = function(bookings, n) { + // nums 初始化为全 0 + let nums = new Array(n).fill(0); + // 构造差分解法 + let df = new Difference(nums); + + for (let booking of bookings) { + // 注意转成数组索引要减一哦 + let i = booking[0] - 1; + let j = booking[1] - 1; + let val = booking[2]; + // 对区间 nums[i..j] 增加 val + df.increment(i, j, val); + } + // 返回最终的结果数组 + return df.result(); +} + +class Difference { + // 差分数组 + constructor(nums) { + if (nums.length <= 0) { + throw new Error('nums array should not be empty'); + } + this.diff = new Array(nums.length).fill(0); + // 构造差分数组 + this.diff[0] = nums[0]; + for (let i = 1; i < nums.length; i++) { + this.diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + increment(i, j, val) { + this.diff[i] += val; + if (j + 1 < this.diff.length) { + this.diff[j + 1] -= val; + } + } + + result() { + let res = new Array(this.diff.length).fill(0); + // 根据差分数组构造结果数组 + res[0] = this.diff[0]; + for (let i = 1; i < this.diff.length; i++) { + res[i] = res[i - 1] + this.diff[i]; + } + return res; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]: + # nums 初始化为全 0 + nums = [0] * n + # 构造差分解法 + df = Difference(nums) + + for booking in bookings: + # 注意转成数组索引要减一哦 + i, j, val = booking[0] - 1, booking[1] - 1, booking[2] + # 对区间 nums[i..j] 增加 val + df.increment(i, j, val) + # 返回最终的结果数组 + return df.result() + + class Difference: + # 差分数组 + def __init__(self, nums: List[int]): + assert len(nums) > 0 + self.diff = [0] * len(nums) + # 构造差分数组 + self.diff[0] = nums[0] + for i in range(1, len(nums)): + self.diff[i] = nums[i] - nums[i - 1] + + # 给闭区间 [i, j] 增加 val(可以是负数) + def increment(self, i: int, j: int, val: int) -> None: + self.diff[i] += val + if j + 1 < len(self.diff): + self.diff[j + 1] -= val + + def result(self) -> List[int]: + res = [0] * len(self.diff) + # 根据差分数组构造结果数组 + res[0] = self.diff[0] + for i in range(1, len(self.diff)): + res[i] = res[i - 1] + self.diff[i] + return res +``` + +https://leetcode.cn/problems/corporate-flight-bookings 的多语言解法👆 + +https://leetcode.cn/problems/count-complete-tree-nodes 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int countNodes(TreeNode* root) { + TreeNode* l = root; + TreeNode* r = root; + // 记录左、右子树的高度 + int hl = 0, hr = 0; + while (l != nullptr) { + l = l->left; + hl++; + } + while (r != nullptr) { + r = r->right; + hr++; + } + // 如果左右子树的高度相同,则是一棵满二叉树 + if (hl == hr) { + return pow(2, hl) - 1; + } + // 如果左右高度不同,则按照普通二叉树的逻辑计算 + return 1 + countNodes(root->left) + countNodes(root->right); + } +}; +``` + +```go +// by chatGPT (go) +func countNodes(root *TreeNode) int { + // 记录左、右子树的高度 + hl, hr := 0, 0 + l, r := root, root + for l != nil { + l = l.Left + hl++ + } + for r != nil { + r = r.Right + hr++ + } + // 如果左右子树的高度相同,则是一棵满二叉树 + if hl == hr { + return int(math.Pow(2, float64(hl))) - 1 + } + // 如果左右高度不同,则按照普通二叉树的逻辑计算 + return 1 + countNodes(root.Left) + countNodes(root.Right) +} +``` + +```java +// by labuladong (java) +class Solution { + public int countNodes(TreeNode root) { + TreeNode l = root, r = root; + // 记录左、右子树的高度 + int hl = 0, hr = 0; + while (l != null) { + l = l.left; + hl++; + } + while (r != null) { + r = r.right; + hr++; + } + // 如果左右子树的高度相同,则是一棵满二叉树 + if (hl == hr) { + return (int) Math.pow(2, hl) - 1; + } + // 如果左右高度不同,则按照普通二叉树的逻辑计算 + return 1 + countNodes(root.left) + countNodes(root.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +var countNodes = function(root) { + let l = root, r = root; + // 记录左、右子树的高度 + let hl = 0, hr = 0; + + while (l !== null) { + l = l.left; + hl++; + } + + while (r !== null) { + r = r.right; + hr++; + } + + // 如果左右子树的高度相同,则是一棵满二叉树 + if (hl === hr) { + return Math.pow(2, hl) - 1; + } + + // 如果左右高度不同,则按照普通二叉树的逻辑计算 + return 1 + countNodes(root.left) + countNodes(root.right); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def countNodes(self, root: Optional[TreeNode]) -> int: + l, r = root, root + hl, hr = 0, 0 + # 记录左、右子树的高度 + while l: + l = l.left + hl += 1 + while r: + r = r.right + hr += 1 + # 如果左右子树的高度相同,则是一棵满二叉树 + if hl == hr: + return 2 ** hl - 1 + # 如果左右高度不同,则按照普通二叉树的逻辑计算 + return 1 + self.countNodes(root.left) + self.countNodes(root.right) +``` + +https://leetcode.cn/problems/count-complete-tree-nodes 的多语言解法👆 + +https://leetcode.cn/problems/count-numbers-with-unique-digits 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int countNumbersWithUniqueDigits(int n) { + if (n == 0) return 1; + if (n == 1) return 10; + int res = 9; + // 计算 n 位数的阶乘,共有几种不同数字的排列组合 + for (int i = 1; i < n; i++) { + res *= (10 - i); + } + // 最后要加上 n - 1 位数的排列组合 + return res + countNumbersWithUniqueDigits(n - 1); + } +}; +``` + +```go +// by chatGPT (go) +// Solution function +func countNumbersWithUniqueDigits(n int) int { + if n == 0 { + return 1 + } + if n == 1 { + return 10 + } + res := 9 + + // 计算 n 位数的阶乘,共有几种不同数字的排列组合 + for i := 1; i < n; i++ { + res *= (10 - i) + } + // 最后要加上 n - 1 位数的排列组合 + return res + countNumbersWithUniqueDigits(n - 1) +} +``` + +```java +// by labuladong (java) +class Solution { + public int countNumbersWithUniqueDigits(int n) { + if (n == 0) return 1; + if (n == 1) return 10; + int res = 9; + // 计算 n 位数的阶乘,共有几种不同数字的排列组合 + for (int i = 1; i < n; i++) { + res *= (10 - i); + } + // 最后要加上 n - 1 位数的排列组合 + return res + countNumbersWithUniqueDigits(n - 1); + } +} +``` + +```javascript +// by chatGPT (javascript) +var countNumbersWithUniqueDigits = function(n) { + if (n == 0) return 1; + if (n == 1) return 10; + var res = 9; + // 计算 n 位数的阶乘,共有几种不同数字的排列组合 + for (var i = 1; i < n; i++) { + res *= (10 - i); + } + // 最后要加上 n - 1 位数的排列组合 + return res + countNumbersWithUniqueDigits(n - 1); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def countNumbersWithUniqueDigits(self, n: int) -> int: + if n == 0: + return 1 + if n == 1: + return 10 + res = 9 + # 计算 n 位数的阶乘,共有几种不同数字的排列组合 + for i in range(1, n): + res *= (10 - i) + # 最后要加上 n - 1 位数的排列组合 + return res + self.countNumbersWithUniqueDigits(n - 1) +``` + +https://leetcode.cn/problems/count-numbers-with-unique-digits 的多语言解法👆 + +https://leetcode.cn/problems/count-of-range-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + int lower, upper; + int count = 0; + vector temp; +public: + int countRangeSum(vector& nums, int lower, int upper) { + this->lower = lower; + this->upper = upper; + vector preSum(nums.size() + 1); + for (int i = 0; i < nums.size(); i++) { + preSum[i + 1] = nums[i] + preSum[i]; + } + sort(preSum, 0, preSum.size()- 1); + return count; + } + + // 归并排序 + void sort(vector& nums, int lo, int hi) { + if (lo == hi) { + // 单个元素不用排序 + return; + } + int mid = lo + (hi - lo) / 2; + // 先对左半部分数组 nums[lo..mid] 排序 + sort(nums, lo, mid); + // 再对右半部分数组 nums[mid+1..hi] 排序 + sort(nums, mid + 1, hi); + // 将两部分有序数组合并成一个有序数组 + merge(nums, lo, mid, hi); + } + + // 归并操作 + void merge(vector& nums, int lo, int mid, int hi) { + temp.assign(mid - lo + 1 + hi - mid, 0); + int i = lo, j = mid + 1, k = 0, start = mid + 1, end = mid + 1; + while (i <= mid) { + while (start <= hi && nums[start] - nums[i] < lower) { + start++; + } + while (end <= hi && nums[end] - nums[i] <= upper) { + end++; + } + count += end - start; + while (j <= hi && nums[j] < nums[i]) { + temp[k++] = nums[j++]; + } + temp[k++] = nums[i++]; + } + while (j <= hi) { + temp[k++] = nums[j++]; + } + for (int p = 0; p < k; p++) { + nums[lo + p] = temp[p]; + } + } +}; +``` + +```go +// by chatGPT (go) +// 解法:归并排序 +// 时间复杂度:O(nlogn) +// 空间复杂度:O(n) +func countRangeSum(nums []int, lower int, upper int) int { + // 前缀和数组 + presum := make([]int64, len(nums)+1) + for i := 0; i < len(nums); i++ { + presum[i+1] = int64(nums[i]) + presum[i] + } + + // 归并排序 + temp := make([]int64, len(nums)+1) + return mergeSort(presum, temp, 0, len(presum)-1, lower, upper) +} + +// 将 nums[lo..hi] 分别排序,然后合并成一个有序数组 +func mergeSort(nums, temp []int64, lo, hi, lower, upper int) int { + if lo == hi { + return 0 + } + mid := lo + (hi-lo)/2 + count := mergeSort(nums, temp, lo, mid, lower, upper) + mergeSort(nums, temp, mid+1, hi, lower, upper) + + // 统计左闭右开区间 [start, end) 中的元素落在 [lower, upper] 中 + start, end := mid+1, mid+1 + for i := lo; i <= mid; i++ { + for start <= hi && nums[start]-nums[i] < int64(lower) { + start++ + } + for end <= hi && nums[end]-nums[i] <= int64(upper) { + end++ + } + count += end - start + } + + // 合并有序数组 + copy(temp[lo:hi+1], nums[lo:hi+1]) + i, j := lo, mid+1 + for k := lo; k <= hi; k++ { + if i == mid+1 { + nums[k] = temp[j] + j++ + } else if j == hi+1 || temp[i] <= temp[j] { + nums[k] = temp[i] + i++ + } else { + nums[k] = temp[j] + j++ + } + } + + return count +} +``` + +```java +// by labuladong (java) +class Solution { + int lower, upper; + + public int countRangeSum(int[] nums, int lower, int upper) { + this.lower = lower; + this.upper = upper; + long[] preSum = new long[nums.length + 1]; + for (int i = 0; i < nums.length; i++) { + preSum[i + 1] = (long) nums[i] + preSum[i]; + } + sort(preSum); + return count; + } + + // 用于辅助合并有序数组 + private long[] temp; + private int count = 0; + + public void sort(long[] nums) { + // 先给辅助数组开辟内存空间 + temp = new long[nums.length]; + // 排序整个数组(原地修改) + sort(nums, 0, nums.length - 1); + } + + // 定义:将子数组 nums[lo..hi] 进行排序 + private void sort(long[] nums, int lo, int hi) { + if (lo == hi) { + // 单个元素不用排序 + return; + } + // 这样写是为了防止溢出,效果等同于 (hi + lo) / 2 + int mid = lo + (hi - lo) / 2; + // 先对左半部分数组 nums[lo..mid] 排序 + sort(nums, lo, mid); + // 再对右半部分数组 nums[mid+1..hi] 排序 + sort(nums, mid + 1, hi); + // 将两部分有序数组合并成一个有序数组 + merge(nums, lo, mid, hi); + } + + // 将 nums[lo..mid] 和 nums[mid+1..hi] 这两个有序数组合并成一个有序数组 + private void merge(long[] nums, int lo, int mid, int hi) { + // 先把 nums[lo..hi] 复制到辅助数组中 + // 以便合并后的结果能够直接存入 nums + for (int i = lo; i <= hi; i++) { + temp[i] = nums[i]; + } + + // 这段代码会超时 + // for (int i = lo; i <= mid; i++) { + // // 在区间 [mid + 1, hi] 中寻找 lower <= delta <= upper 的元素 + // for (int k = mid + 1; k <= hi; k++) { + // long delta = nums[k] - nums[i]; + // if (delta <= upper && delta >= lower) { + // count++; + // } + // } + // } + + // 进行效率优化 + // 维护左闭右开区间 [start, end) 中的元素落在 [lower, upper] 中 + int start = mid + 1, end = mid + 1; + for (int i = lo; i <= mid; i++) { + while (start <= hi && nums[start] - nums[i] < lower) { + start++; + } + while (end <= hi && nums[end] - nums[i] <= upper) { + end++; + } + count += end - start; + } + + // 数组双指针技巧,合并两个有序数组 + int i = lo, j = mid + 1; + for (int p = lo; p <= hi; p++) { + if (i == mid + 1) { + // 左半边数组已全部被合并 + nums[p] = temp[j++]; + } else if (j == hi + 1) { + // 右半边数组已全部被合并 + nums[p] = temp[i++]; + } else if (temp[i] > temp[j]) { + nums[p] = temp[j++]; + } else { + nums[p] = temp[i++]; + } + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var countRangeSum = function(nums, lower, upper) { + let count = 0; + const n = nums.length; + const preSum = new Array(n + 1).fill(0); + for (let i = 0; i < n; i++) { + preSum[i + 1] = nums[i] + preSum[i]; + } + + mergeSort(preSum, 0, n); + + return count; + + function mergeSort(nums, left, right) { + if (left >= right) { + return; + } + const mid = left + Math.floor((right - left) / 2); + mergeSort(nums, left, mid); + mergeSort(nums, mid + 1, right); + + let i = left; + let j = mid + 1; + let k = 0; + let t = 0; + const temp = new Array(right - left + 1); + + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + // 统计区间和 count + while (t <= right && nums[t] - nums[i] < lower) { + t++; + } + while (k <= right && nums[k] - nums[i] <= upper) { + k++; + } + count += k - t; + temp[i - left] = nums[i++]; + } else { + temp[i - left] = nums[j++]; + } + } + while (i <= mid) { + temp[i - left] = nums[i++]; + } + while (j <= right) { + temp[i - left] = nums[j++]; + } + for (let p = left; p <= right; p++) { + nums[p] = temp[p - left]; + } + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.lower, self.upper = 0, 0 + + def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int: + # 定义全局变量 + self.lower, self.upper = lower, upper + # 前缀和数组 + preSum = [0] * (len(nums) + 1) + for i in range(len(nums)): + preSum[i + 1] = nums[i] + preSum[i] + # 排序 + self.sort(preSum) + return self.count + + def sort(self, nums): + # 定义辅助合并数组 + self.temp = [0] * len(nums) + # 定义计数器 + self.count = 0 + # 调用递归排序方法 + self._sort(nums, 0, len(nums) - 1) + + def _sort(self, nums, lo, hi): + # 排序子数组nums[lo..hi] + if lo == hi: + return + mid = lo + (hi - lo) // 2 + self._sort(nums, lo, mid) + self._sort(nums, mid + 1, hi) + self._merge(nums, lo, mid, hi) + + def _merge(self, nums, lo, mid, hi): + # 合并两个有序子数组 nums[lo..mid] 和 nums[mid+1..hi] + + # 先将 nums[lo..hi] 复制到临时数组 temp 中 + for i in range(lo, hi + 1): + self.temp[i] = nums[i] + + # 左右两部分计数器 start 和 end + start, end = mid + 1, mid + 1 + for i in range(lo, mid + 1): + while start <= hi and nums[start] - nums[i] < self.lower: + start += 1 + while end <= hi and nums[end] - nums[i] <= self.upper: + end += 1 + # 更新计数器 + self.count += end - start + + # 数组合并,双指针技巧 + i, j = lo, mid + 1 + for p in range(lo, hi + 1): + if i > mid: + nums[p] = self.temp[j] + j += 1 + elif j > hi: + nums[p] = self.temp[i] + i += 1 + elif self.temp[i] < self.temp[j]: + nums[p] = self.temp[i] + i += 1 + else: + nums[p] = self.temp[j] + j += 1 +``` + +https://leetcode.cn/problems/count-of-range-sum 的多语言解法👆 + +https://leetcode.cn/problems/count-primes 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int countPrimes(int n) { + vector isPrime(n, true); // vector instead of array + for (int i = 2; i * i < n; i++) + if (isPrime[i]) + for (int j = i * i; j < n; j += i) + isPrime[j] = false; + + int count = 0; + for (int i = 2; i < n; i++) + if (isPrime[i]) count++; + + return count; + } +}; +``` + +```go +// by chatGPT (go) +func countPrimes(n int) int { + // create boolean array with default value true + isPrime := make([]bool, n) + for i := range isPrime { + isPrime[i] = true + } + + // loop through all numbers up to the square root of n + for i := 2; i*i < n; i++ { + if isPrime[i] { + // loop through multiples of i and mark them as not prime + for j := i * i; j < n; j += i { + isPrime[j] = false + } + } + } + + // count number of primes + count := 0 + for i := 2; i < n; i++ { + if isPrime[i] { + count++ + } + } + + return count +} +``` + +```java +// by labuladong (java) +class Solution { + public int countPrimes(int n) { + boolean[] isPrime = new boolean[n]; + Arrays.fill(isPrime, true); + for (int i = 2; i * i < n; i++) + if (isPrime[i]) + for (int j = i * i; j < n; j += i) + isPrime[j] = false; + + int count = 0; + for (int i = 2; i < n; i++) + if (isPrime[i]) count++; + + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number} + */ +var countPrimes = function(n) { + const isPrime = new Array(n).fill(true); + for (let i = 2; i * i < n; i++) { + if (isPrime[i]) { + for (let j = i * i; j < n; j += i) { + isPrime[j] = false; + } + } + } + let count = 0; + for (let i = 2; i < n; i++) { + if (isPrime[i]) count++; + } + return count; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def countPrimes(self, n: int) -> int: + isPrime = [True]*n + for i in range(2, int(n**0.5)+1): + if isPrime[i]: + for j in range(i*i, n, i): + isPrime[j] = False + + count = 0 + for i in range(2, n): + if isPrime[i]: + count += 1 + + return count +``` + +https://leetcode.cn/problems/count-primes 的多语言解法👆 + +https://leetcode.cn/problems/count-sub-islands 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int countSubIslands(vector>& grid1, vector>& grid2) { + int m = grid1.size(), n = grid1[0].size(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid1[i][j] == 0 && grid2[i][j] == 1) { + // 这个岛屿肯定不是子岛,淹掉 + dfs(grid2, i, j); + } + } + } + // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid2[i][j] == 1) { + res++; + dfs(grid2, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(vector>& grid, int i, int j) { + int m = grid.size(), n = grid[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 0) { + return; + } + + grid[i][j] = 0; + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func countSubIslands(grid1 [][]int, grid2 [][]int) int { + m, n := len(grid1), len(grid1[0]) + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid1[i][j] == 0 && grid2[i][j] == 1 { + // 这个岛屿肯定不是子岛,淹掉 + dfs(grid2, i, j) + } + } + } + // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量 + res := 0 + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid2[i][j] == 1 { + res++ + dfs(grid2, i, j) + } + } + } + return res +} + +// 从 (i, j) 开始,将与之相邻的陆地都变成海水 +func dfs(grid [][]int, i, j int) { + m, n := len(grid), len(grid[0]) + if i < 0 || j < 0 || i >= m || j >= n { + return + } + if grid[i][j] == 0 { + return + } + + grid[i][j] = 0 + dfs(grid, i+1, j) + dfs(grid, i, j+1) + dfs(grid, i-1, j) + dfs(grid, i, j-1) +} +``` + +```java +// by labuladong (java) +class Solution { + public int countSubIslands(int[][] grid1, int[][] grid2) { + int m = grid1.length, n = grid1[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid1[i][j] == 0 && grid2[i][j] == 1) { + // 这个岛屿肯定不是子岛,淹掉 + dfs(grid2, i, j); + } + } + } + // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid2[i][j] == 1) { + res++; + dfs(grid2, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 0) { + return; + } + + grid[i][j] = 0; + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} grid1 + * @param {number[][]} grid2 + * @return {number} + */ +var countSubIslands = function(grid1, grid2) { + const m = grid1.length, n = grid1[0].length; + + // 淹没非子岛屿 + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (grid1[i][j] == 0 && grid2[i][j] == 1) { + dfs(grid2, i, j); + } + } + } + + // 计算子岛屿数量 + let res = 0; + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (grid2[i][j] == 1) { + res++; + dfs(grid2, i, j); + } + } + } + return res; +}; + +// 深度优先遍历 +function dfs(grid, i, j) { + const m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 0) { + return; + } + + grid[i][j] = 0; + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); +} +``` + +```python +# by chatGPT (python) +class Solution: + def countSubIslands(self, grid1: List[List[int]], grid2: List[List[int]]) -> int: + m, n = len(grid1), len(grid1[0]) + + # 遍历 grid2,将非子岛的岛屿淹掉 + for i in range(m): + for j in range(n): + if grid1[i][j] == 0 and grid2[i][j] == 1: + self.dfs(grid2, i, j) + + # 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量 + res = 0 + for i in range(m): + for j in range(n): + if grid2[i][j] == 1: + res += 1 + self.dfs(grid2, i, j) + + return res + + # 从 (i, j) 开始,将与之相邻的陆地都变成海水 + def dfs(self, grid: List[List[int]], i: int, j: int) -> None: + m, n = len(grid), len(grid[0]) + if i < 0 or j < 0 or i >= m or j >= n: + return + if grid[i][j] == 0: + return + + grid[i][j] = 0 + self.dfs(grid, i + 1, j) + self.dfs(grid, i, j + 1) + self.dfs(grid, i - 1, j) + self.dfs(grid, i, j - 1) +``` + +https://leetcode.cn/problems/count-sub-islands 的多语言解法👆 + +https://leetcode.cn/problems/couples-holding-hands 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minSwapsCouples(vector& row) { + int n = row.size(); + UF uf(n); + for (int i = 0; i < n; i += 2) { + // 将两人的 couple_id 进行连接 + uf.Union(row[i] / 2, row[i + 1] / 2); + } + // 和连通分量的差即为需要交换的次数 + return n - uf.Count(); + } +}; + +// 并查集算法模板 +class UF { +private: + // 记录连通分量个数 + int count; + // 存储若干棵树 + vector parent; +public: + UF(int n) { + this->count = n; + parent = vector(n); + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + /* 将 p 和 q 连通 */ + void Union(int p, int q) { + int rootP = Find(p); + int rootQ = Find(q); + if (rootP == rootQ) + return; + + parent[rootQ] = rootP; + count--; + } + + /* 判断 p 和 q 是否互相连通 */ + bool Connected(int p, int q) { + int rootP = Find(p); + int rootQ = Find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + int Find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + int Count() { + return count; + } +}; +``` + +```go +// by chatGPT (go) +func minSwapsCouples(row []int) int { + n := len(row) + uf := NewUF(n) + for i := 0; i < n; i += 2 { + // 将两人的 couple_id 进行连接 + uf.Union(row[i]/2, row[i+1]/2) + } + // 和连通分量的差即为需要交换的次数 + return n - uf.Count() +} + +type UF struct { + // 记录连通分量个数 + count int + // 存储若干棵树 + parent []int +} + +func NewUF(n int) *UF { + uf := &UF{} + uf.count = n + uf.parent = make([]int, n) + for i := 0; i < n; i++ { + uf.parent[i] = i + } + return uf +} + +/* 将 p 和 q 连通 */ +func (uf *UF) Union(p, q int) { + rootP := uf.find(p) + rootQ := uf.find(q) + if rootP == rootQ { + return + } + uf.parent[rootQ] = rootP + uf.count-- +} + +/* 判断 p 和 q 是否互相连通 */ +func (uf *UF) Connected(p, q int) bool { + rootP := uf.find(p) + rootQ := uf.find(q) + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ +} + +/* 返回节点 x 的根节点 */ +func (uf *UF) find(x int) int { + for uf.parent[x] != x { + // 进行路径压缩 + uf.parent[x] = uf.parent[uf.parent[x]] + x = uf.parent[x] + } + return x +} + +func (uf *UF) Count() int { + return uf.count +} +``` + +```java +// by labuladong (java) +class Solution { + public int minSwapsCouples(int[] row) { + int n = row.length; + UF uf = new UF(n); + for (int i = 0; i < n; i += 2) { + // 将两人的 couple_id 进行连接 + uf.union(row[i] / 2, row[i + 1] / 2); + } + // 和连通分量的差即为需要交换的次数 + return n - uf.count(); + } +} + +// 并查集算法模板 +class UF { + // 记录连通分量个数 + private int count; + // 存储若干棵树 + private int[] parent; + + public UF(int n) { + this.count = n; + parent = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + /* 将 p 和 q 连通 */ + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + parent[rootQ] = rootP; + count--; + } + + /* 判断 p 和 q 是否互相连通 */ + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + private int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + public int count() { + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} row + * @return {number} + */ +var minSwapsCouples = function(row) { + const n = row.length; + const uf = new UF(n); + for (let i = 0; i < n; i += 2) { + // 将两人的 couple_id 进行连接 + uf.union(Math.floor(row[i] / 2), Math.floor(row[i + 1] / 2)); + } + // 和连通分量的差即为需要交换的次数 + return n - uf.count(); +}; + +// 并查集算法模板 +class UF { + // 记录连通分量个数 + #count; + // 存储若干棵树 + #parent; + + constructor(n) { + this.#count = n; + this.#parent = new Array(n).fill().map((_, i) => i); + } + + /* 将 p 和 q 连通 */ + union(p, q) { + const rootP = this.find(p); + const rootQ = this.find(q); + if (rootP === rootQ) { + return; + } + + this.#parent[rootQ] = rootP; + this.#count--; + } + + /* 判断 p 和 q 是否互相连通 */ + connected(p, q) { + const rootP = this.find(p); + const rootQ = this.find(q); + // 处于同一棵树上的节点,相互连通 + return rootP === rootQ; + } + + /* 返回节点 x 的根节点 */ + find(x) { + while (this.#parent[x] !== x) { + // 进行路径压缩 + this.#parent[x] = this.#parent[this.#parent[x]]; + x = this.#parent[x]; + } + return x; + } + + count() { + return this.#count; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def minSwapsCouples(self, row: List[int]) -> int: + n = len(row) + uf = UF(n) + for i in range(0, n, 2): + # 将两人的 couple_id 进行连接 + uf.union(row[i] // 2, row[i + 1] // 2) + # 和连通分量的差即为需要交换的次数 + return n - uf.count() + +class UF: + def __init__(self, n: int): + self.count = n + self.parent = [i for i in range(n)] + + def union(self, p: int, q: int) -> None: + rootP = self.find(p) + rootQ = self.find(q) + if rootP == rootQ: + return + + self.parent[rootQ] = rootP + self.count -= 1 + + def connected(self, p: int, q: int) -> bool: + rootP = self.find(p) + rootQ = self.find(q) + # 处于同一棵树上的节点,相互连通 + return rootP == rootQ + + def find(self, x: int) -> int: + while self.parent[x] != x: + # 进行路径压缩 + self.parent[x] = self.parent[self.parent[x]] + x = self.parent[x] + return x + + def count(self) -> int: + return self.count +``` + +https://leetcode.cn/problems/couples-holding-hands 的多语言解法👆 + +https://leetcode.cn/problems/course-schedule 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 记录一次 traverse 递归经过的节点 + vector onPath; + // 记录遍历过的节点,防止走回头路 + vector visited; + // 记录图中是否有环 + bool hasCycle = false; + +public: + bool canFinish(int numCourses, vector>& prerequisites) { + vector> graph = buildGraph(numCourses, prerequisites); + + visited.resize(numCourses, false); + onPath.resize(numCourses, false); + + for (int i = 0; i < numCourses; i++) { + // 遍历图中的所有节点 + traverse(graph, i); + } + // 只要没有循环依赖可以完成所有课程 + return !hasCycle; + } + + void traverse(vector>& graph, int s) { + if (onPath[s]) { + // 出现环 + hasCycle = true; + /** + ![](../pictures/拓扑排序/4.jpeg) + */ + } + + if (visited[s] || hasCycle) { + // 如果已经找到了环,也不用再遍历了 + return; + } + // 前序遍历代码位置 + visited[s] = true; + onPath[s] = true; + for (int t : graph[s]) { + traverse(graph, t); + } + // 后序遍历代码位置 + onPath[s] = false; + } + + vector> buildGraph(int numCourses, vector>& prerequisites) { + // 图中共有 numCourses 个节点 + vector> graph(numCourses); + for (auto edge : prerequisites) { + int from = edge[1]; + int to = edge[0]; + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].push_back(to); + } + return graph; + } +}; +``` + +```go +// by chatGPT (go) +func canFinish(numCourses int, prerequisites [][]int) bool { + // 记录一次 traverse 递归经过的节点 + onPath := make([]bool, numCourses) + // 记录遍历过的节点,防止走回头路 + visited := make([]bool, numCourses) + // 记录图中是否有环 + hasCycle := false + + graph := buildGraph(numCourses, prerequisites) + + for i := 0; i < numCourses; i++ { + // 遍历图中的所有节点 + traverse(graph, i, &hasCycle, visited, onPath) + } + // 只要没有循环依赖可以完成所有课程 + return !hasCycle +} + +func traverse(graph []LinkedList, s int, hasCycle *bool, visited, onPath []bool) { + if onPath[s] { + // 出现环 + *hasCycle = true + /** + ![](../pictures/拓扑排序/4.jpeg) + */ + } + + if visited[s] || *hasCycle { + // 如果已经找到了环,也不用再遍历了 + return + } + // 前序遍历代码位置 + visited[s] = true + onPath[s] = true + for _, t := range graph[s].list { + traverse(graph, t, hasCycle, visited, onPath) + } + // 后序遍历代码位置 + onPath[s] = false +} + +type LinkedList struct { + list []int +} + +func buildGraph(numCourses int, prerequisites [][]int) []LinkedList { + // 图中共有 numCourses 个节点 + graph := make([]LinkedList, numCourses) + for i := 0; i < numCourses; i++ { + graph[i] = LinkedList{list: []int{}} + } + for _, edge := range prerequisites { + from := edge[1] + to := edge[0] + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].list = append(graph[from].list, to) + } + return graph +} +``` + +```java +// by labuladong (java) +class Solution { + // 记录一次 traverse 递归经过的节点 + boolean[] onPath; + // 记录遍历过的节点,防止走回头路 + boolean[] visited; + // 记录图中是否有环 + boolean hasCycle = false; + + public boolean canFinish(int numCourses, int[][] prerequisites) { + List[] graph = buildGraph(numCourses, prerequisites); + + visited = new boolean[numCourses]; + onPath = new boolean[numCourses]; + + for (int i = 0; i < numCourses; i++) { + // 遍历图中的所有节点 + traverse(graph, i); + } + // 只要没有循环依赖可以完成所有课程 + return !hasCycle; + } + + void traverse(List[] graph, int s) { + if (onPath[s]) { + // 出现环 + hasCycle = true; + /** + ![](../pictures/拓扑排序/4.jpeg) + */ + } + + if (visited[s] || hasCycle) { + // 如果已经找到了环,也不用再遍历了 + return; + } + // 前序遍历代码位置 + visited[s] = true; + onPath[s] = true; + for (int t : graph[s]) { + traverse(graph, t); + } + // 后序遍历代码位置 + onPath[s] = false; + } + + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 图中共有 numCourses 个节点 + List[] graph = new LinkedList[numCourses]; + for (int i = 0; i < numCourses; i++) { + graph[i] = new LinkedList<>(); + } + for (int[] edge : prerequisites) { + int from = edge[1]; + int to = edge[0]; + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].add(to); + } + return graph; + } +} +``` + +```javascript +// by chatGPT (javascript) +var canFinish = function(numCourses, prerequisites) { + // 记录一次 traverse 递归经过的节点 + const onPath = new Array(numCourses).fill(false); + // 记录遍历过的节点,防止走回头路 + const visited = new Array(numCourses).fill(false); + // 记录图中是否有环 + let hasCycle = false; + + const graph = buildGraph(numCourses, prerequisites); + + for (let i = 0; i < numCourses; i++) { + // 遍历图中的所有节点 + traverse(graph, i); + } + + // 只要没有循环依赖可以完成所有课程 + return !hasCycle; + + function traverse(graph, s) { + if (onPath[s]) { + // 出现环 + hasCycle = true; + /** + ![](../pictures/拓扑排序/4.jpeg) + */ + return; + } + + if (visited[s] || hasCycle) { + // 如果已经找到了环,也不用再遍历了 + return; + } + // 前序遍历代码位置 + visited[s] = true; + onPath[s] = true; + for (let t of graph[s]) { + traverse(graph, t); + } + // 后序遍历代码位置 + onPath[s] = false; + } + + function buildGraph(numCourses, prerequisites) { + // 图中共有 numCourses 个节点 + const graph = new Array(numCourses).map(() => []); + for (let edge of prerequisites) { + const from = edge[1]; + const to = edge[0]; + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].push(to); + } + return graph; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: + # 记录一次 traverse 递归经过的节点 + onPath = [False] * numCourses + # 记录遍历过的节点,防止走回头路 + visited = [False] * numCourses + # 记录图中是否有环 + hasCycle = [False] + + graph = self.buildGraph(numCourses, prerequisites) + + def traverse(s): + if onPath[s]: + # 出现环 + hasCycle[0] = True + """ + + ![](../pictures/拓扑排序/4.jpeg) + """ + if visited[s] or hasCycle[0]: + # 如果已经找到了环,也不用再遍历了 + return + # 前序遍历代码位置 + visited[s] = True + onPath[s] = True + for t in graph[s]: + traverse(t) + # 后序遍历代码位置 + onPath[s] = False + + for i in range(numCourses): + # 遍历图中的所有节点 + traverse(i) + + # 只要没有循环依赖可以完成所有课程 + return not hasCycle[0] + + def buildGraph(self, numCourses, prerequisites): + # 图中共有 numCourses 个节点 + graph = [[] for _ in range(numCourses)] + for from_, to in prerequisites: + # 修完课程 from_ 才能修课程 to + # 在图中添加一条从 from_ 指向 to 的有向边 + graph[from_].append(to) + return graph +``` + +https://leetcode.cn/problems/course-schedule 的多语言解法👆 + +https://leetcode.cn/problems/course-schedule-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector findOrder(int numCourses, vector>& prerequisites) { + // 建图,和环检测算法相同 + vector> graph(numCourses); + // 计算入度,和环检测算法相同 + vector indegree(numCourses, 0); + for (auto& edge : prerequisites) { + int from = edge[1], to = edge[0]; + graph[from].push_back(to); + indegree[to]++; + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + queue q; + for (int i = 0; i < numCourses; i++) { + if (indegree[i] == 0) { + q.push(i); + } + } + + // 记录拓扑排序结果 + vector res; + // 开始执行 BFS 算法 + while (!q.empty()) { + int cur = q.front(); + q.pop(); + // 弹出节点的顺序即为拓扑排序结果 + res.push_back(cur); + for (int next : graph[cur]) { + indegree[next]--; + if (indegree[next] == 0) { + q.push(next); + } + } + } + + if (res.size() != numCourses) { + // 存在环,拓扑排序不存在 + return {}; + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func findOrder(numCourses int, prerequisites [][]int) []int { + // 建图,和环检测算法相同 + graph := buildGraph(numCourses, prerequisites) + // 计算入度,和环检测算法相同 + indegree := make([]int, numCourses) + for _, edge := range prerequisites { + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + from, to := edge[1], edge[0] + _, to := edge[1], edge[0] + indegree[to]++ + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + q := make([]int, 0) + for i := 0; i < numCourses; i++ { + if indegree[i] == 0 { + q = append(q, i) + } + } + + // 记录拓扑排序结果 + res := make([]int, numCourses) + // 记录遍历节点的顺序(索引) + count := 0 + // 开始执行 BFS 算法 + for len(q) > 0 { + cur := q[0] + q = q[1:] + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur + count++ + for _, next := range graph[cur] { + indegree[next]-- + if indegree[next] == 0 { + q = append(q, next) + } + } + } + + if count != numCourses { + // 存在环,拓扑排序不存在 + return []int{} + } + + return res +} + +// 建图函数 +func buildGraph(numCourses int, prerequisites [][]int) []([]int) { + // 图中共有 numCourses 个节点 + graph := make([]([]int), numCourses) + for i := 0; i < numCourses; i++ { + graph[i] = make([]int, 0) + } + for _, edge := range prerequisites { + from, to := edge[1], edge[0] + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from] = append(graph[from], to) + } + return graph +} +``` + +```java +// by labuladong (java) +class Solution { + // 主函数 + public int[] findOrder(int numCourses, int[][] prerequisites) { + // 建图,和环检测算法相同 + List[] graph = buildGraph(numCourses, prerequisites); + // 计算入度,和环检测算法相同 + int[] indegree = new int[numCourses]; + for (int[] edge : prerequisites) { + int from = edge[1], to = edge[0]; + indegree[to]++; + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + Queue q = new LinkedList<>(); + for (int i = 0; i < numCourses; i++) { + if (indegree[i] == 0) { + q.offer(i); + /** + ![](../pictures/拓扑排序/6.jpeg) + */ + } + } + + // 记录拓扑排序结果 + int[] res = new int[numCourses]; + // 记录遍历节点的顺序(索引) + int count = 0; + // 开始执行 BFS 算法 + while (!q.isEmpty()) { + int cur = q.poll(); + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur; + count++; + for (int next : graph[cur]) { + /** + ![](../pictures/拓扑排序/7.jpeg) + */ + indegree[next]--; + if (indegree[next] == 0) { + q.offer(next); + } + } + } + + if (count != numCourses) { + // 存在环,拓扑排序不存在 + return new int[]{}; + } + + return res; + } + + // 建图函数 + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 图中共有 numCourses 个节点 + List[] graph = new LinkedList[numCourses]; + for (int i = 0; i < numCourses; i++) { + graph[i] = new LinkedList<>(); + } + for (int[] edge : prerequisites) { + int from = edge[1], to = edge[0]; + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].add(to); + } + return graph; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findOrder = function(numCourses, prerequisites) { + // 建图,和环检测算法相同 + const graph = buildGraph(numCourses, prerequisites); + // 计算入度,和环检测算法相同 + const indegree = new Array(numCourses).fill(0); + for (const [from, to] of prerequisites) { + indegree[to]++; + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + const q = []; + for (let i = 0; i < numCourses; i++) { + if (indegree[i] === 0) { + q.push(i); + /** + ![](../pictures/拓扑排序/6.jpeg) + */ + } + } + + // 记录拓扑排序结果 + const res = new Array(numCourses); + // 记录遍历节点的顺序(索引) + let count = 0; + // 开始执行 BFS 算法 + while (q.length > 0) { + const cur = q.shift(); + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur; + count++; + for (const next of graph[cur]) { + /** + ![](../pictures/拓扑排序/7.jpeg) + */ + indegree[next]--; + if (indegree[next] === 0) { + q.push(next); + } + } + } + + if (count !== numCourses) { + // 存在环,拓扑排序不存在 + return []; + } + + return res; +} + +// 建图函数 +function buildGraph(numCourses, prerequisites) { + // 图中共有 numCourses 个节点 + const graph = new Array(numCourses).fill().map(() => []); + for (const [from, to] of prerequisites) { + // 修完课程 from 才能修课程 to + // 在图中添加一条从 from 指向 to 的有向边 + graph[from].push(to); + } + return graph; +} +``` + +```python +# by chatGPT (python) +class Solution: + def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]: + # 建图,和环检测算法相同 + graph = self.buildGraph(numCourses, prerequisites) + # 计算入度,和环检测算法相同 + indegree = [0] * numCourses + for edge in prerequisites: + from_course, to_course = edge[1], edge[0] + indegree[to_course] += 1 + + # 根据入度初始化队列中的节点,和环检测算法相同 + q = [] + for i in range(numCourses): + if indegree[i] == 0: + q.append(i) + + # 记录拓扑排序结果 + res = [] + # 记录遍历节点的顺序(索引) + count = 0 + # 开始执行 BFS 算法 + while q: + cur = q.pop(0) + # 弹出节点的顺序即为拓扑排序结果 + res.append(cur) + count += 1 + for next_course in graph[cur]: + indegree[next_course] -= 1 + if indegree[next_course] == 0: + q.append(next_course) + + if count != numCourses: + # 存在环,拓扑排序不存在 + return [] + + return res + + # 建图函数 + def buildGraph(self, numCourses: int, prerequisites: List[List[int]]) -> List[List[int]]: + # 图中共有 numCourses 个节点 + graph = [[] for _ in range(numCourses)] + for edge in prerequisites: + from_course, to_course = edge[1], edge[0] + # 修完课程 from 才能修课程 to + # 在图中添加一条从 from 指向 to 的有向边 + graph[from_course].append(to_course) + return graph +``` + +https://leetcode.cn/problems/course-schedule-ii 的多语言解法👆 + +https://leetcode.cn/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector printNumbers(int n) { + // 先生成 n 位 9 + int max = 0; + for (int i = 0; i < n; i++) { + max = 10 * max + 9; + } + // 填入结果数组 + vector res(max); + for (int i = 1; i <= max; i++) { + res[i - 1] = i; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func printNumbers(n int) []int { + // 先生成 n 位 9 + max := 0 + for i := 0; i < n; i++ { + max = 10 * max + 9 + } + // 填入结果数组 + res := make([]int, max) + for i := 1; i <= max; i++ { + res[i-1] = i + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] printNumbers(int n) { + // 先生成 n 位 9 + int max = 0; + for (int i = 0; i < n; i++) { + max = 10 * max + 9; + } + // 填入结果数组 + int[] res = new int[max]; + for (int i = 1; i <= max; i++) { + res[i - 1] = i; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number[]} + */ +var printNumbers = function(n) { + // 先生成 n 位 9 + let max = 0; + for (let i = 0; i < n; i++) { + max = 10 * max + 9; + } + // 填入结果数组 + let res = new Array(max); + for (let i = 1; i <= max; i++) { + res[i - 1] = i; + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def printNumbers(self, n: int) -> List[int]: + # 先生成 n 位 9 + max_num = 0 + for i in range(n): + max_num = max_num * 10 + 9 + + # 填入结果数组 + res = [] + for i in range(1, max_num + 1): + res.append(i) + + return res +``` + +https://leetcode.cn/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/delete-node-in-a-bst 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + TreeNode* deleteNode(TreeNode* root, int key) { + if (root == nullptr) { + return nullptr; + } + if (root->val == key) { + // 这两个 if 把情况 1 和 2 都正确处理了 + if (root->left == nullptr) { + return root->right; + } + if (root->right == nullptr) { + return root->left; + } + // 处理情况 3 + // 获得右子树最小的节点 + TreeNode* minNode = getMin(root->right); + // 删除右子树最小的节点 + root->right = deleteNode(root->right, minNode->val); + // 用右子树最小的节点替换 root 节点 + minNode->left = root->left; + minNode->right = root->right; + root = minNode; + } else if (root->val > key) { + root->left = deleteNode(root->left, key); + } else if (root->val < key) { + root->right = deleteNode(root->right, key); + } + return root; + } + + TreeNode* getMin(TreeNode* node) { + // BST 最左边的就是最小的 + while (node->left != nullptr) { + node = node->left; + } + return node; + } +}; +``` + +```go +// by chatGPT (go) +func deleteNode(root *TreeNode, key int) *TreeNode { + if root == nil { + return nil + } + if root.Val == key { + // 这两个 if 把情况 1 和 2 都正确处理了 + if root.Left == nil { + return root.Right + } + if root.Right == nil { + return root.Left + } + // 处理情况 3 + // 获得右子树最小的节点 + minNode := getMin(root.Right) + // 删除右子树最小的节点 + root.Right = deleteNode(root.Right, minNode.Val) + // 用右子树最小的节点替换 root 节点 + minNode.Left = root.Left + minNode.Right = root.Right + root = minNode + } else if root.Val > key { + root.Left = deleteNode(root.Left, key) + } else if root.Val < key { + root.Right = deleteNode(root.Right, key) + } + return root +} + +func getMin(node *TreeNode) *TreeNode { + // BST 最左边的就是最小的 + for node.Left != nil { + node = node.Left + } + return node +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode deleteNode(TreeNode root, int key) { + if (root == null) return null; + if (root.val == key) { + // 这两个 if 把情况 1 和 2 都正确处理了 + if (root.left == null) return root.right; + if (root.right == null) return root.left; + // 处理情况 3 + // 获得右子树最小的节点 + TreeNode minNode = getMin(root.right); + // 删除右子树最小的节点 + root.right = deleteNode(root.right, minNode.val); + // 用右子树最小的节点替换 root 节点 + minNode.left = root.left; + minNode.right = root.right; + root = minNode; + } else if (root.val > key) { + root.left = deleteNode(root.left, key); + } else if (root.val < key) { + root.right = deleteNode(root.right, key); + } + return root; + } + + TreeNode getMin(TreeNode node) { + // BST 最左边的就是最小的 + while (node.left != null) node = node.left; + return node; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @param {number} key + * @return {TreeNode} + */ +var deleteNode = function(root, key) { + if (root == null) return null; + if (root.val == key) { + // 这两个 if 把情况 1 和 2 都正确处理了 + if (root.left == null) return root.right; + if (root.right == null) return root.left; + // 处理情况 3 + // 获得右子树最小的节点 + let minNode = getMin(root.right); + // 删除右子树最小的节点 + root.right = deleteNode(root.right, minNode.val); + // 用右子树最小的节点替换 root 节点 + minNode.left = root.left; + minNode.right = root.right; + root = minNode; + } else if (root.val > key) { + root.left = deleteNode(root.left, key); + } else if (root.val < key) { + root.right = deleteNode(root.right, key); + } + return root; +}; + +var getMin = function(node) { + // BST 最左边的就是最小的 + while (node.left != null) node = node.left; + return node; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def deleteNode(self, root: TreeNode, key: int) -> TreeNode: + if not root: + return None + if root.val == key: + # 这两个 if 把情况 1 和 2 都正确处理了 + if not root.left: + return root.right + if not root.right: + return root.left + # 处理情况 3 + # 获得右子树最小的节点 + min_node = self.getMin(root.right) + # 删除右子树最小的节点 + root.right = self.deleteNode(root.right, min_node.val) + # 用右子树最小的节点替换 root 节点 + min_node.left = root.left + min_node.right = root.right + root = min_node + elif root.val > key: + root.left = self.deleteNode(root.left, key) + elif root.val < key: + root.right = self.deleteNode(root.right, key) + return root + + def getMin(self, node: TreeNode) -> TreeNode: + # BST 最左边的就是最小的 + while node.left: + node = node.left + return node +``` + +https://leetcode.cn/problems/delete-node-in-a-bst 的多语言解法👆 + +https://leetcode.cn/problems/delete-operation-for-two-strings 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minDistance(string s1, string s2) { + int m = s1.length(), n = s2.length(); + // 复用前文计算 lcs 长度的函数 + int lcs = longestCommonSubsequence(s1, s2); + return m - lcs + n - lcs; + } + + // 计算最长公共子序列的长度 + int longestCommonSubsequence(string s1, string s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + vector> dp(m + 1, vector(n + 1, 0)); + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1[i - 1] == s2[j - 1]) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + return dp[m][n]; + } +}; +``` + +```go +// by chatGPT (go) +func minDistance(s1 string, s2 string) int { + m, n := len(s1), len(s2) + // 复用前文计算 lcs 长度的函数 + lcs := longestCommonSubsequence(s1, s2) + return m - lcs + n - lcs +} + +// 计算最长公共子序列的长度 +func longestCommonSubsequence(s1 string, s2 string) int { + m, n := len(s1), len(s2) + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + dp := make([][]int, m+1) + for i := 0; i <= m; i++ { + dp[i] = make([]int, n+1) + } + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + // 现在 i 和 j 从 1 开始,所以要减一 + if s1[i-1] == s2[j-1] { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i-1][j-1] + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j-1], dp[i-1][j]) + } + } + } + return dp[m][n] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 复用前文计算 lcs 长度的函数 + int lcs = longestCommonSubsequence(s1, s2); + return m - lcs + n - lcs; + } + + // 计算最长公共子序列的长度 + int longestCommonSubsequence(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + int[][] dp = new int[m + 1][n + 1]; + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + return dp[m][n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var minDistance = function(s1, s2) { + const m = s1.length, n = s2.length; + // 复用前文计算 lcs 长度的函数 + const lcs = longestCommonSubsequence(s1, s2); + return m - lcs + n - lcs; +}; + +// 计算最长公共子序列的长度 +function longestCommonSubsequence(s1, s2) { + const m = s1.length, n = s2.length; + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + const dp = Array.from(Array(m + 1), () => new Array(n + 1).fill(0)); + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + return dp[m][n]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minDistance(self, s1: str, s2: str) -> int: + m, n = len(s1), len(s2) + # 复用前文计算 lcs 长度的函数 + lcs = self.longestCommonSubsequence(s1, s2) + return m - lcs + n - lcs + + # 计算最长公共子序列的长度 + def longestCommonSubsequence(self, s1: str, s2: str) -> int: + m, n = len(s1), len(s2) + # 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + dp = [[0] * (n + 1) for _ in range(m + 1)] + + for i in range(1, m + 1): + for j in range(1, n + 1): + # 现在 i 和 j 从 1 开始,所以要减一 + if s1[i - 1] == s2[j - 1]: + # s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1] + else: + # s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]) + return dp[m][n] +``` + +https://leetcode.cn/problems/delete-operation-for-two-strings 的多语言解法👆 + +https://leetcode.cn/problems/design-twitter 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Twitter { +private: + // Tweet 类 + class Tweet { + private: + int id; + // 时间戳用于对信息流按照时间排序 + int timestamp; + // 指向下一条 tweet,类似单链表结构 + Tweet *next; + + public: + Tweet(int id) { + this->id = id; + // 新建一条 tweet 时记录并更新时间戳 + this->timestamp = globalTime++; + } + + int getId() { + return id; + } + + int getTimestamp() { + return timestamp; + } + + Tweet* getNext() { + return next; + } + + void setNext(Tweet *next) { + this->next = next; + } + }; + + // 用户类 + class User { + private: + // 记录该用户的 id 以及发布的 tweet + int id; + Tweet *tweetHead; + // 记录该用户的关注者 + unordered_set followedUserSet; + + public: + User(int id) { + this->id = id; + this->tweetHead = nullptr; + this->followedUserSet = unordered_set(); + } + + int getId() { + return id; + } + + Tweet* getTweetHead() { + return tweetHead; + } + + unordered_set getFollowedUserSet() { + return followedUserSet; + } + + bool equals(User* other) { + return this->id == other->id; + } + + // 关注其他人 + void follow(User *other) { + followedUserSet.insert(other); + } + + // 取关其他人 + void unfollow(User *other) { + followedUserSet.erase(other); + } + + // 发布一条 tweet + void post(Tweet *tweet) { + // 把新发布的 tweet 作为链表头节点 + tweet->setNext(tweetHead); + tweetHead = tweet; + } + }; + + // 全局时间戳 + int globalTime = 0; + // 记录用户 ID 到用户示例的映射 + unordered_map idToUser; + +public: + void postTweet(int userId, int tweetId) { + // 如果这个用户还不存在,新建用户 + if (idToUser.find(userId) == idToUser.end()) { + idToUser[userId] = new User(userId); + } + User* user = idToUser[userId]; + user->post(new Tweet(tweetId)); + } + + vector getNewsFeed(int userId) { + vector res = vector(); + if (idToUser.find(userId) == idToUser.end()) { + return res; + } + // 获取该用户关注的用户列表 + User* user = idToUser[userId]; + unordered_set followedUserSet = user->getFollowedUserSet(); + // 每个用户的 tweet 是一条按时间排序的链表 + // 现在执行合并多条有序链表的逻辑,找出时间线中的最近 10 条动态 + auto cmp = [](Tweet* a, Tweet* b) -> bool { + // 按照每条 tweet 的发布时间降序排序(最近发布的排在事件流前面) + return b->getTimestamp() < a->getTimestamp(); + }; + priority_queue, decltype(cmp)> pq(cmp); + // 该用户自己的 tweet 也在时间线内 + if (user->getTweetHead() != nullptr) { + pq.push(user->getTweetHead()); + } + for (User* other : followedUserSet) { + if (other->getTweetHead() != nullptr) { + pq.push(other->getTweetHead()); + } + } + // 合并多条有序链表 + int count = 0; + while (!pq.empty() && count < 10) { + Tweet* tweet = pq.top(); + pq.pop(); + res.push_back(tweet->getId()); + if (tweet->getNext() != nullptr) { + pq.push(tweet->getNext()); + } + count++; + } + return res; + } + + void follow(int followerId, int followeeId) { + // 如果用户还不存在,则新建用户 + if (idToUser.find(followerId) == idToUser.end()) { + idToUser[followerId] = new User(followerId); + } + if (idToUser.find(followeeId) == idToUser.end()) { + idToUser[followeeId] = new User(followeeId); + } + + User* follower = idToUser[followerId]; + User* followee = idToUser[followeeId]; + // 关注者关注被关注者 + follower->follow(followee); + } + + void unfollow(int followerId, int followeeId) { + if (idToUser.find(followerId) == idToUser.end() || idToUser.find(followeeId) == idToUser.end()) { + return; + } + User* follower = idToUser[followerId]; + User* followee = idToUser[followeeId]; + // 关注者取关被关注者 + follower->unfollow(followee); + } +}; +``` + +```go +// by chatGPT (go) +type Twitter struct { + // 全局时间戳 + globalTime int + // 记录用户 ID 到用户示例的映射 + idToUser map[int]*User +} + +// Tweet 类 +type Tweet struct { + id int + // 时间戳用于对信息流按照时间排序 + timestamp int + // 指向下一条 tweet,类似单链表结构 + next *Tweet +} + +func NewTweet(id int) *Tweet { + return &Tweet{ + id: id, + timestamp: globalTime, + } +} + +// User 类 +type User struct { + // 记录该用户的 id 以及发布的 tweet + id int + tweetHead *Tweet + // 记录该用户的关注者 + followedUsers map[int]*User +} + +func NewUser(id int) *User { + return &User{ + id: id, + followedUsers: make(map[int]*User), + } +} + +func (u *User) Post(tweet *Tweet) { + // 把新发布的 tweet 作为链表头节点 + tweet.next = u.tweetHead + u.tweetHead = tweet +} + +func (u *User) Follow(other *User) { + u.followedUsers[other.id] = other +} + +func (u *User) Unfollow(other *User) { + delete(u.followedUsers, other.id) +} + +func (u *User) Eq(other *User) bool { + return u.id == other.id +} + +func (t *Twitter) PostTweet(userId int, tweetId int) { + // 如果这个用户还不存在,新建用户 + if _, ok := t.idToUser[userId]; !ok { + t.idToUser[userId] = NewUser(userId) + } + user := t.idToUser[userId] + user.Post(NewTweet(tweetId)) +} + +func (t *Twitter) GetNewsFeed(userId int) []int { + res := make([]int, 0) + if _, ok := t.idToUser[userId]; !ok { + return res + } + // 获取该用户关注的用户列表 + user := t.idToUser[userId] + followedUserSet := user.followedUsers + // 每个用户的 tweet 是一条按时间排序的链表 + // 现在执行合并多条有序链表的逻辑,找出时间线中的最近 10 条动态 + pq := make(PriorityQueue, 0) + // 按照每条 tweet 的发布时间降序排序(最近发布的排在事件流前面) + heap.Init(&pq) + // 该用户自己的 tweet 也在时间线内 + if user.tweetHead != nil { + heap.Push(&pq, user.tweetHead) + } + for _, other := range followedUserSet { + if other.tweetHead != nil { + heap.Push(&pq, other.tweetHead) + } + } + // 合并多条有序链表 + count := 0 + for pq.Len() > 0 && count < 10 { + tweet := heap.Pop(&pq).(*Tweet) + res = append(res, tweet.id) + if tweet.next != nil { + heap.Push(&pq, tweet.next) + } + count++ + } + return res +} + +func (t *Twitter) Follow(followerId int, followeeId int) { + // 如果用户还不存在,则新建用户 + if _, ok := t.idToUser[followerId]; !ok { + t.idToUser[followerId] = NewUser(followerId) + } + if _, ok := t.idToUser[followeeId]; !ok { + t.idToUser[followeeId] = NewUser(followeeId) + } + + follower := t.idToUser[followerId] + followee := t.idToUser[followeeId] + // 关注者关注被关注者 + follower.Follow(followee) +} + +func (t *Twitter) Unfollow(followerId int, followeeId int) { + if _, ok := t.idToUser[followerId]; !ok || _, ok := t.idToUser[followeeId]; !ok { + return + } + follower := t.idToUser[followerId] + followee := t.idToUser[followeeId] + // 关注者取关被关注者 + follower.Unfollow(followee) +} + +type PriorityQueue []*Tweet + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].timestamp > pq[j].timestamp +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Pop() interface{} { + n := len(*pq) + item := (*pq)[n-1] + *pq = (*pq)[:n-1] + return item +} + +func (pq *PriorityQueue) Push(item interface{}) { + *pq = append(*pq, item.(*Tweet)) +} +``` + +```java +// by labuladong (java) +class Twitter { + // 全局时间戳 + int globalTime = 0; + // 记录用户 ID 到用户示例的映射 + HashMap idToUser = new HashMap<>(); + + // Tweet 类 + class Tweet { + private int id; + // 时间戳用于对信息流按照时间排序 + private int timestamp; + // 指向下一条 tweet,类似单链表结构 + private Tweet next; + + public Tweet(int id) { + this.id = id; + // 新建一条 tweet 时记录并更新时间戳 + this.timestamp = globalTime++; + } + + public int getId() { + return id; + } + + public int getTimestamp() { + return timestamp; + } + + public Tweet getNext() { + return next; + } + + public void setNext(Tweet next) { + this.next = next; + } + } + + // 用户类 + class User { + // 记录该用户的 id 以及发布的 tweet + private int id; + private Tweet tweetHead; + // 记录该用户的关注者 + private HashSet followedUserSet; + + public User(int id) { + this.id = id; + this.tweetHead = null; + this.followedUserSet = new HashSet<>(); + } + + public int getId() { + return id; + } + + public Tweet getTweetHead() { + return tweetHead; + } + + public HashSet getFollowedUserSet() { + return followedUserSet; + } + + public boolean equals(User other) { + return this.id == other.id; + } + + // 关注其他人 + public void follow(User other) { + followedUserSet.add(other); + } + + // 取关其他人 + public void unfollow(User other) { + followedUserSet.remove(other); + } + + // 发布一条 tweet + public void post(Tweet tweet) { + // 把新发布的 tweet 作为链表头节点 + tweet.setNext(tweetHead); + tweetHead = tweet; + } + } + + public void postTweet(int userId, int tweetId) { + // 如果这个用户还不存在,新建用户 + if (!idToUser.containsKey(userId)) { + idToUser.put(userId, new User(userId)); + } + User user = idToUser.get(userId); + user.post(new Tweet(tweetId)); + } + + public List getNewsFeed(int userId) { + List res = new LinkedList<>(); + if (!idToUser.containsKey(userId)) { + return res; + } + // 获取该用户关注的用户列表 + User user = idToUser.get(userId); + Set followedUserSet = user.getFollowedUserSet(); + // 每个用户的 tweet 是一条按时间排序的链表 + // 现在执行合并多条有序链表的逻辑,找出时间线中的最近 10 条动态 + PriorityQueue pq = new PriorityQueue<>((a, b) -> { + // 按照每条 tweet 的发布时间降序排序(最近发布的排在事件流前面) + return b.timestamp - a.timestamp; + }); + // 该用户自己的 tweet 也在时间线内 + if (user.getTweetHead() != null) { + pq.offer(user.getTweetHead()); + } + for (User other : followedUserSet) { + if (other.getTweetHead() != null) { + pq.offer(other.tweetHead); + } + } + // 合并多条有序链表 + int count = 0; + while (!pq.isEmpty() && count < 10) { + Tweet tweet = pq.poll(); + res.add(tweet.getId()); + if (tweet.getNext() != null) { + pq.offer(tweet.getNext()); + } + count++; + } + return res; + } + + public void follow(int followerId, int followeeId) { + // 如果用户还不存在,则新建用户 + if (!idToUser.containsKey(followerId)) { + idToUser.put(followerId, new User(followerId)); + } + if (!idToUser.containsKey(followeeId)) { + idToUser.put(followeeId, new User(followeeId)); + } + + User follower = idToUser.get(followerId); + User followee = idToUser.get(followeeId); + // 关注者关注被关注者 + follower.follow(followee); + } + + public void unfollow(int followerId, int followeeId) { + if (!idToUser.containsKey(followerId) || !idToUser.containsKey(followeeId)) { + return; + } + User follower = idToUser.get(followerId); + User followee = idToUser.get(followeeId); + // 关注者取关被关注者 + follower.unfollow(followee); + } +} +``` + +```javascript +// by chatGPT (javascript) +var Twitter = function() { + // 全局时间戳 + let globalTime = 0; + // 记录用户 ID 到用户示例的映射 + const idToUser = new Map(); + + // Tweet 类 + function Tweet(id) { + this.id = id; + // 时间戳用于对信息流按照时间排序 + this.timestamp = globalTime++; + // 指向下一条 tweet,类似单链表结构 + this.next = null; + } + + // 用户类 + function User(id) { + // 记录该用户的 id 以及发布的 tweet + this.id = id; + this.tweetHead = null; + // 记录该用户的关注者 + this.followedUserSet = new Set(); + } + + User.prototype.equals = function(other) { + return this.id === other.id; + } + + // 关注其他人 + User.prototype.follow = function(other) { + this.followedUserSet.add(other); + } + + // 取关其他人 + User.prototype.unfollow = function(other) { + this.followedUserSet.delete(other); + } + + // 发布一条 tweet + User.prototype.post = function(tweet) { + // 把新发布的 tweet 作为链表头节点 + tweet.next = this.tweetHead; + this.tweetHead = tweet; + } + + this.postTweet = function(userId, tweetId) { + // 如果这个用户还不存在,新建用户 + if (!idToUser.has(userId)) { + idToUser.set(userId, new User(userId)); + } + const user = idToUser.get(userId); + user.post(new Tweet(tweetId)); + }; + + this.getNewsFeed = function(userId) { + const res = []; + if (!idToUser.has(userId)) { + return res; + } + // 获取该用户关注的用户列表 + const user = idToUser.get(userId); + const followedUserSet = user.followedUserSet; + // 每个用户的 tweet 是一条按时间排序的链表 + // 现在执行合并多条有序链表的逻辑,找出时间线中的最近 10 条动态 + const pq = new PriorityQueue((a, b) => { + // 按照每条 tweet 的发布时间降序排序(最近发布的排在事件流前面) + return b.timestamp - a.timestamp; + }); + // 该用户自己的 tweet 也在时间线内 + if (user.tweetHead !== null) { + pq.offer(user.tweetHead); + } + for (const other of followedUserSet) { + if (other.tweetHead !== null) { + pq.offer(other.tweetHead); + } + } + // 合并多条有序链表 + let count = 0; + while (!pq.isEmpty() && count < 10) { + const tweet = pq.poll(); + res.push(tweet.id); + if (tweet.next !== null) { + pq.offer(tweet.next); + } + count++; + } + return res; + }; + + this.follow = function(followerId, followeeId) { + // 如果用户还不存在,则新建用户 + if (!idToUser.has(followerId)) { + idToUser.set(followerId, new User(followerId)); + } + if (!idToUser.has(followeeId)) { + idToUser.set(followeeId, new User(followeeId)); + } + + const follower = idToUser.get(followerId); + const followee = idToUser.get(followeeId); + // 关注者关注被关注者 + follower.follow(followee); + }; + + this.unfollow = function(followerId, followeeId) { + if (!idToUser.has(followerId) || !idToUser.has(followeeId)) { + return; + } + const follower = idToUser.get(followerId); + const followee = idToUser.get(followeeId); + // 关注者取关被关注者 + follower.unfollow(followee); + }; +}; + +// 优先队列实现 +class PriorityQueue { + constructor(comparator) { + this.heap = []; + this.comparator = comparator; + } + + /** + * 上浮操作 + * @param {number} index - 上浮节点的下标 + */ + swim(index) { + let currentIndex = index; + while (currentIndex > 0) { + const parentIndex = Math.floor((currentIndex - 1) / 2); + if (this.comparator(this.heap[currentIndex], this.heap[parentIndex]) >= 0) { + break; + } + [this.heap[currentIndex], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[currentIndex]]; + currentIndex = parentIndex; + } + } + + /** + * 下沉操作 + * @param {number} index - 下沉节点的下标 + */ + sink(index) { + let currentIndex = index; + while (currentIndex * 2 + 1 < this.heap.length) { + const leftIndex = currentIndex * 2 + 1; + const rightIndex = currentIndex * 2 + 2 < this.heap.length ? currentIndex * 2 + 2 : leftIndex; + const smallerIndex = this.comparator(this.heap[leftIndex], this.heap[rightIndex]) <= 0 ? leftIndex : rightIndex; + if (this.comparator(this.heap[currentIndex], this.heap[smallerIndex]) <= 0) { + break; + } + [this.heap[currentIndex], this.heap[smallerIndex]] = [this.heap[smallerIndex], this.heap[currentIndex]]; + currentIndex = smallerIndex; + } + } + + /** + * 插入元素 + * @param {*} value - 插入的值 + */ + offer(value) { + this.heap.push(value); + this.swim(this.heap.length - 1); + } + + /** + * 弹出堆顶元素 + * @return {*} 堆顶元素 + */ + poll() { + if (this.size() === 0) { + return null; + } + if (this.size() === 1) { + return this.heap.pop(); + } + const top = this.heap[0]; + this.heap[0] = this.heap.pop(); + this.sink(0); + return top; + } + + /** + * 获取堆大小 + * @return {number} 堆大小 + */ + size() { + return this.heap.length; + } +} +``` + +```python +# by chatGPT (python) +class Twitter: + def __init__(self): + # 全局时间戳 + self.globalTime = 0 + # 记录用户 ID 到用户示例的映射 + self.idToUser = {} + + # Tweet 类 + class Tweet: + def __init__(self, id: int): + self.id = id + # 时间戳用于对信息流按照时间排序 + self.timestamp = Twitter.globalTime + Twitter.globalTime += 1 + # 指向下一条 tweet,类似单链表结构 + self.next = None + + def get_id(self) -> int: + return self.id + + def get_timestamp(self) -> int: + return self.timestamp + + def get_next(self): + return self.next + + def set_next(self, next_tweet): + self.next = next_tweet + + # 用户类 + class User: + def __init__(self, id: int): + # 记录该用户的 id 以及发布的 tweet + self.id = id + self.tweet_head = None + # 记录该用户的关注者 + self.followed_user_set = set() + + def get_id(self): + return self.id + + def get_tweet_head(self) -> Tweet: + return self.tweet_head + + def get_followed_user_set(self): + return self.followed_user_set + + def __eq__(self, other): + return self.id == other.id + + # 关注其他人 + def follow(self, other): + self.followed_user_set.add(other) + + # 取关其他人 + def unfollow(self, other): + self.followed_user_set.discard(other) + + # 发布一条 tweet + def post(self, tweet: Tweet): + # 把新发布的 tweet 作为链表头节点 + tweet.set_next(self.tweet_head) + self.tweet_head = tweet + + def postTweet(self, userId: int, tweetId: int) -> None: + # 如果这个用户还不存在,新建用户 + if userId not in self.idToUser: + self.idToUser[userId] = Twitter.User(userId) + user = self.idToUser[userId] + user.post(Twitter.Tweet(tweetId)) + + def getNewsFeed(self, userId: int) -> List[int]: + res = [] + if userId not in self.idToUser: + return res + # 获取该用户关注的用户列表 + user = self.idToUser[userId] + followed_user_set = user.get_followed_user_set() + # 每个用户的 tweet 是一条按时间排序的链表 + # 现在执行合并多条有序链表的逻辑,找出时间线中的最近 10 条动态 + pq = [] + # 该用户自己的 tweet 也在时间线内 + if user.get_tweet_head(): + heappush(pq, (-user.tweet_head.timestamp, user.get_tweet_head())) # 按照每条 tweet 的发布时间降序排序(最近发布的排在事件流前面) + for other in followed_user_set: + if other.get_tweet_head(): + heappush(pq, (-other.tweet_head.timestamp, other.get_tweet_head())) + # 合并多条有序链表 + count = 0 + while pq and count < 10: + _, tweet = heappop(pq) + res.append(tweet.get_id()) + if tweet.get_next(): + heappush(pq, (-tweet.get_next().timestamp, tweet.get_next())) + count += 1 + return res + + def follow(self, followerId: int, followeeId: int) -> None: + # 如果用户还不存在,则新建用户 + if followerId not in self.idToUser: + self.idToUser[followerId] = Twitter.User(followerId) + if followeeId not in self.idToUser: + self.idToUser[followeeId] = Twitter.User(followeeId) + + follower = self.idToUser[followerId] + followee = self.idToUser[followeeId] + # 关注者关注被关注者 + follower.follow(followee) + + def unfollow(self, followerId: int, followeeId: int) -> None: + if followerId not in self.idToUser or followeeId not in self.idToUser: + return + follower = self.idToUser[followerId] + followee = self.idToUser[followeeId] + # 关注者取关被关注者 + follower.unfollow(followee) +``` + +https://leetcode.cn/problems/design-twitter 的多语言解法👆 + +https://leetcode.cn/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + char firstUniqChar(string s) { + int count[26] = {0}; + for (char c : s) { + // 将字符转化成数字 + count[c - 'a']++; + } + for (int i = 0; i < s.length(); i++) { + char c = s[i]; + if (count[c - 'a'] == 1) { + // 第一个出现一次的字符 + return c; + } + } + return ' '; + } +}; +``` + +```go +// by chatGPT (go) +func firstUniqChar(s string) byte { + count := make([]int, 26) + for _, c := range s { + // 将字符转化成数字 + count[c-'a']++ + } + for _, c := range s { + if count[c-'a'] == 1 { + // 第一个出现一次的字符 + return byte(c) + } + } + return ' ' +} +``` + +```java +// by labuladong (java) +class Solution { + public char firstUniqChar(String s) { + int[] count = new int[26]; + for (char c : s.toCharArray()) { + // 将字符转化成数字 + count[c - 'a']++; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (count[c - 'a'] == 1) { + // 第一个出现一次的字符 + return c; + } + } + return ' '; + } +} +``` + +```javascript +// by chatGPT (javascript) +var firstUniqChar = function(s) { + var count = new Array(26).fill(0); + for (var i = 0; i < s.length; i++) { + // 将字符转化成数字 + count[s.charCodeAt(i) - 97]++; + } + for (var i = 0; i < s.length; i++) { + var c = s.charAt(i); + if (count[c.charCodeAt(0) - 97] == 1) { + // 第一个出现一次的字符 + return c; + } + } + return ' '; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def firstUniqChar(self, s: str) -> str: + count = [0] * 26 + for c in s: + # 将字符转化成数字 + count[ord(c) - ord('a')] += 1 + for c in s: + if count[ord(c) - ord('a')] == 1: + # 第一个出现一次的字符 + return c + return ' ' +``` + +https://leetcode.cn/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/diameter-of-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + int maxDiameter = 0; + +public: + int diameterOfBinaryTree(TreeNode* root) { + maxDepth(root); + return maxDiameter; + } + + int maxDepth(TreeNode* root) { + if (root == nullptr) { + return 0; + } + int leftMax = maxDepth(root->left); + int rightMax = maxDepth(root->right); + // 后序遍历位置顺便计算最大直径 + maxDiameter = max(maxDiameter, leftMax + rightMax); + return 1 + max(leftMax, rightMax); + } +}; + +// 这是一种简单粗暴,但是效率不高的解法 +class BadSolution { +public: + int diameterOfBinaryTree(TreeNode* root) { + if (root == nullptr) { + return 0; + } + // 计算出左右子树的最大高度 + int leftMax = maxDepth(root->left); + int rightMax = maxDepth(root->right); + // root 这个节点的直径 + int res = leftMax + rightMax; + // 递归遍历 root->left 和 root->right 两个子树 + return max(res, + max(diameterOfBinaryTree(root->left), + diameterOfBinaryTree(root->right))); + } + + int maxDepth(TreeNode* root) { + if (root == nullptr) { + return 0; + } + int leftMax = maxDepth(root->left); + int rightMax = maxDepth(root->right); + return 1 + max(leftMax, rightMax); + } +}; +``` + +```go +// by mario_huang (go) +var maxDiameter int + +func diameterOfBinaryTree(root *TreeNode) int { + // 记录最大直径的长度 + maxDiameter = 0 + maxDepth(root) + return maxDiameter +} + +func maxDepth(root *TreeNode) int { + if root == nil { + return 0 + } + leftMax := maxDepth(root.Left) + rightMax := maxDepth(root.Right) + // 后序位置,顺便计算最大直径 + myDiameter := leftMax + rightMax + maxDiameter = max(maxDiameter, myDiameter) + return max(leftMax, rightMax) + 1 +} + +// 这是一种简单粗暴,但是效率不高的解法 +func diameterOfBinaryTree(root *TreeNode) int { + if root == nil { + return 0 + } + // 计算出左右子树的最大高度 + maxDepth := func(root *TreeNode) int { + if root == nil { + return 0 + } + leftMax := maxDepth(root.Left) + rightMax := maxDepth(root.Right) + return 1 + max(leftMax, rightMax) + } + leftMax := maxDepth(root.Left) + rightMax := maxDepth(root.Right) + // root 这个节点的直径 + res := leftMax + rightMax + // 递归遍历 root.Left 和 root.Right 两个子树 + return max(res, + max(diameterOfBinaryTree(root.Left), + diameterOfBinaryTree(root.Right))) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + int maxDiameter = 0; + + public int diameterOfBinaryTree(TreeNode root) { + maxDepth(root); + return maxDiameter; + } + + int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + // 后序遍历位置顺便计算最大直径 + maxDiameter = Math.max(maxDiameter, leftMax + rightMax); + return 1 + Math.max(leftMax, rightMax); + } +} + +// 这是一种简单粗暴,但是效率不高的解法 +class BadSolution { + public int diameterOfBinaryTree(TreeNode root) { + if (root == null) { + return 0; + } + // 计算出左右子树的最大高度 + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + // root 这个节点的直径 + int res = leftMax + rightMax; + // 递归遍历 root.left 和 root.right 两个子树 + return Math.max(res, + Math.max(diameterOfBinaryTree(root.left), + diameterOfBinaryTree(root.right))); + } + + int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + return 1 + Math.max(leftMax, rightMax); + } +} +``` + +```javascript +// by chatGPT (javascript) +var diameterOfBinaryTree = function(root) { + let maxDiameter = 0; + + const maxDepth = (root) => { + if (root == null) { + return 0; + } + let leftMax = maxDepth(root.left); + let rightMax = maxDepth(root.right); + // 后序遍历位置顺便计算最大直径 + maxDiameter = Math.max(maxDiameter, leftMax + rightMax); + return 1 + Math.max(leftMax, rightMax); + }; + + maxDepth(root); + return maxDiameter; +} + +// 这是一种简单粗暴,但是效率不高的解法 +var BadSolution = function() {}; + +BadSolution.prototype.diameterOfBinaryTree = function(root) { + if (root == null) { + return 0; + } + // 计算出左右子树的最大高度 + let leftMax = this.maxDepth(root.left); + let rightMax = this.maxDepth(root.right); + // root 这个节点的直径 + let res = leftMax + rightMax; + // 递归遍历 root.left 和 root.right 两个子树 + return Math.max(res, + Math.max(this.diameterOfBinaryTree(root.left), + this.diameterOfBinaryTree(root.right))); +}; + +BadSolution.prototype.maxDepth = function(root) { + if (root == null) { + return 0; + } + let leftMax = this.maxDepth(root.left); + let rightMax = this.maxDepth(root.right); + return 1 + Math.max(leftMax, rightMax); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.maxDiameter = 0 + + def diameterOfBinaryTree(self, root: TreeNode) -> int: + self.maxDepth(root) + return self.maxDiameter + + def maxDepth(self, root: TreeNode) -> int: + if not root: + return 0 + leftMax = self.maxDepth(root.left) + rightMax = self.maxDepth(root.right) + # 后序遍历位置顺便计算最大直径 + self.maxDiameter = max(self.maxDiameter, leftMax + rightMax) + return 1 + max(leftMax, rightMax) + +# 这是一种简单粗暴,但是效率不高的解法 +class BadSolution: + def diameterOfBinaryTree(self, root: TreeNode) -> int: + if not root: + return 0 + # 计算出左右子树的最大高度 + leftMax = self.maxDepth(root.left) + rightMax = self.maxDepth(root.right) + # root 这个节点的直径 + res = leftMax + rightMax + # 递归遍历 root.left 和 root.right 两个子树 + return max(res, max(self.diameterOfBinaryTree(root.left), self.diameterOfBinaryTree(root.right))) + + def maxDepth(self, root: TreeNode) -> int: + if not root: + return 0 + leftMax = self.maxDepth(root.left) + rightMax = self.maxDepth(root.right) + return 1 + max(leftMax, rightMax) +``` + +https://leetcode.cn/problems/diameter-of-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector exchange(vector& nums) { + // 维护 nums[0..slow) 都是奇数 + int fast = 0, slow = 0; + while (fast < nums.size()) { + if (nums[fast] % 2 == 1) { + // fast 遇到奇数,把 nums[fast] 换到 nums[slow] + int temp = nums[slow]; + nums[slow] = nums[fast]; + nums[fast] = temp; + slow++; + } + fast++; + } + return nums; + } +}; +``` + +```go +// by chatGPT (go) +func exchange(nums []int) []int { + // 维护 nums[0..slow) 都是奇数 + fast, slow := 0, 0 + for fast < len(nums) { + if nums[fast]%2 == 1 { + // fast 遇到奇数,把 nums[fast] 换到 nums[slow] + temp := nums[slow] + nums[slow] = nums[fast] + nums[fast] = temp + slow++ + } + fast++ + } + return nums +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] exchange(int[] nums) { + // 维护 nums[0..slow) 都是奇数 + int fast = 0, slow = 0; + while (fast < nums.length) { + if (nums[fast] % 2 == 1) { + // fast 遇到奇数,把 nums[fast] 换到 nums[slow] + int temp = nums[slow]; + nums[slow] = nums[fast]; + nums[fast] = temp; + slow++; + } + fast++; + } + return nums; + } +} +``` + +```javascript +// by chatGPT (javascript) +var exchange = function(nums) { + // 维护 nums[0..slow) 都是奇数 + let fast = 0, slow = 0; + while (fast < nums.length) { + if (nums[fast] % 2 === 1) { + // fast 遇到奇数,把 nums[fast] 换到 nums[slow] + let temp = nums[slow]; + nums[slow] = nums[fast]; + nums[fast] = temp; + slow++; + } + fast++; + } + return nums; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def exchange(self, nums: List[int]) -> List[int]: + # 维护 nums[0..slow) 都是奇数 + fast, slow = 0, 0 + while fast < len(nums): + if nums[fast] % 2 == 1: + # fast 遇到奇数,把 nums[fast] 换到 nums[slow] + nums[slow], nums[fast] = nums[fast], nums[slow] + slow += 1 + fast += 1 + return nums +``` + +https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof 的多语言解法👆 + +https://leetcode.cn/problems/different-ways-to-add-parentheses 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + unordered_map> memo; +public: + vector diffWaysToCompute(string input) { + // 避免重复计算 + if (memo.count(input)) { + return memo[input]; + } + vector res; + for (int i = 0; i < input.length(); i++) { + char c = input[i]; + // 扫描算式 input 中的运算符 + if (c == '-' || c == '*' || c == '+') { + /******分******/ + // 以运算符为中心,分割成两个字符串,分别递归计算 + vector left = diffWaysToCompute(input.substr(0, i)); + vector right = diffWaysToCompute(input.substr(i + 1)); + /******治******/ + // 通过子问题的结果,合成原问题的结果 + for (int a : left) + for (int b : right) + if (c == '+') + res.push_back(a + b); + else if (c == '-') + res.push_back(a - b); + else if (c == '*') + res.push_back(a * b); + } + } + // base case + // 如果 res 为空,说明算式是一个数字,没有运算符 + if (res.empty()) { + res.push_back(stoi(input)); + } + // 将结果添加进备忘录 + memo[input] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +func diffWaysToCompute(input string) []int { + memo := make(map[string][]int) + return compute(input, memo) +} + +func compute(input string, memo map[string][]int) []int { + // 避免重复计算 + if val, ok := memo[input]; ok { + return val + } + res := make([]int, 0) + for i := 0; i < len(input); i++ { + c := input[i] + // 扫描算式 input 中的运算符 + if c == '-' || c == '*' || c == '+' { + /******分******/ + // 以运算符为中心,分割成两个字符串,分别递归计算 + left := compute(input[0:i], memo) + right := compute(input[i+1:], memo) + /******治******/ + // 通过子问题的结果,合成原问题的结果 + for _, a := range left { + for _, b := range right { + if c == '+' { + res = append(res, a+b) + } else if c == '-' { + res = append(res, a-b) + } else if c == '*' { + res = append(res, a*b) + } + } + } + } + } + // base case + // 如果 res 为空,说明算式是一个数字,没有运算符 + if len(res) == 0 { + n, _ := strconv.Atoi(input) + res = append(res, n) + } + // 将结果添加进备忘录 + memo[input] = res + return res +} +``` + +```java +// by labuladong (java) +class Solution { + + HashMap> memo = new HashMap<>(); + + public List diffWaysToCompute(String input) { + // 避免重复计算 + if (memo.containsKey(input)) { + return memo.get(input); + } + List res = new LinkedList<>(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + // 扫描算式 input 中的运算符 + if (c == '-' || c == '*' || c == '+') { + /******分******/ + // 以运算符为中心,分割成两个字符串,分别递归计算 + List + left = diffWaysToCompute(input.substring(0, i)); + List + right = diffWaysToCompute(input.substring(i + 1)); + /******治******/ + // 通过子问题的结果,合成原问题的结果 + for (int a : left) + for (int b : right) + if (c == '+') + res.add(a + b); + else if (c == '-') + res.add(a - b); + else if (c == '*') + res.add(a * b); + } + } + // base case + // 如果 res 为空,说明算式是一个数字,没有运算符 + if (res.isEmpty()) { + res.add(Integer.parseInt(input)); + } + // 将结果添加进备忘录 + memo.put(input, res); + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var diffWaysToCompute = function(input) { + let memo = new Map(); + const compute = (input) => { + if (memo.has(input)) { + return memo.get(input); + } + let res = []; + for (let i = 0; i < input.length; i++) { + let c = input.charAt(i); + // 扫描算式 input 中的运算符 + if (c == '-' || c == '*' || c == '+') { + /******分******/ + // 以运算符为中心,分割成两个字符串,分别递归计算 + let left = compute(input.substring(0, i)); + let right = compute(input.substring(i + 1)); + /******治******/ + // 通过子问题的结果,合成原问题的结果 + for (let a of left) { + for (let b of right) { + if (c == '+') { + res.push(a + b); + } else if (c == '-') { + res.push(a - b); + } else if (c == '*') { + res.push(a * b); + } + } + } + } + } + // base case + // 如果 res 为空,说明算式是一个数字,没有运算符 + if (res.length == 0) { + res.push(parseInt(input)); + } + // 将结果添加进备忘录 + memo.set(input, res); + return res; + }; + return compute(input); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.memo = {} + + def diffWaysToCompute(self, input: str) -> List[int]: + # 避免重复计算 + if input in self.memo: + return self.memo[input] + res = [] + for i in range(len(input)): + c = input[i] + # 扫描算式 input 中的运算符 + if c in ['-', '*', '+']: + # 以运算符为中心,分割成两个字符串,分别递归计算 + left = self.diffWaysToCompute(input[:i]) + right = self.diffWaysToCompute(input[i+1:]) + # 通过子问题的结果,合成原问题的结果 + for a in left: + for b in right: + if c == '+': + res.append(a + b) + elif c == '-': + res.append(a - b) + elif c == '*': + res.append(a * b) + # base case + # 如果 res 为空,说明算式是一个数字,没有运算符 + if not res: + res.append(int(input)) + # 将结果添加进备忘录 + self.memo[input] = res + return res +``` + +https://leetcode.cn/problems/different-ways-to-add-parentheses 的多语言解法👆 + +https://leetcode.cn/problems/dungeon-game 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int calculateMinimumHP(vector>& grid) { + int m = grid.size(); + int n = grid[0].size(); + // 备忘录中都初始化为 -1 + memo = vector>(m, vector(n, -1)); + + return dp(grid, 0, 0); + } + + // 备忘录,消除重叠子问题 + vector> memo; + + /* 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 */ + int dp(vector>& grid, int i, int j) { + int m = grid.size(); + int n = grid[0].size(); + // base case + if (i == m - 1 && j == n - 1) { + return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; + } + if (i == m || j == n) { + return INT_MAX; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 状态转移逻辑 + int res = min( + dp(grid, i, j + 1), + dp(grid, i + 1, j) + ) - grid[i][j]; + // 骑士的生命值至少为 1 + memo[i][j] = res <= 0 ? 1 : res; + + return memo[i][j]; + } +}; +``` + +```go +// by chatGPT (go) +import "math" + +func calculateMinimumHP(grid [][]int) int { + m := len(grid) + n := len(grid[0]) + // 备忘录中都初始化为 -1 + memo := make([][]int, m) + for i := range memo { + memo[i] = make([]int, n) + for j := range memo[i] { + memo[i][j] = -1 + } + } + + return dp(grid, 0, 0, memo) +} + +/* 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 */ +func dp(grid [][]int, i, j int, memo [][]int) int { + m := len(grid) + n := len(grid[0]) + // base case + if i == m-1 && j == n-1 { + if grid[i][j] >= 0 { + return 1 + } else { + return -grid[i][j] + 1 + } + } + if i == m || j == n { + return math.MaxInt32 + } + // 避免重复计算 + if memo[i][j] != -1 { + return memo[i][j] + } + // 状态转移逻辑 + res := int(math.Min( + float64(dp(grid, i, j+1, memo)), + float64(dp(grid, i+1, j, memo)), + )) - grid[i][j] + // 骑士的生命值至少为 1 + if res <= 0 { + memo[i][j] = 1 + } else { + memo[i][j] = res + } + + return memo[i][j] +} +``` + +```java +// by labuladong (java) +class Solution { + + public int calculateMinimumHP(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + // 备忘录中都初始化为 -1 + memo = new int[m][n]; + for (int[] row : memo) { + Arrays.fill(row, -1); + } + + return dp(grid, 0, 0); + } + + // 备忘录,消除重叠子问题 + int[][] memo; + + /* 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 */ + int dp(int[][] grid, int i, int j) { + int m = grid.length; + int n = grid[0].length; + // base case + if (i == m - 1 && j == n - 1) { + return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; + } + if (i == m || j == n) { + return Integer.MAX_VALUE; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 状态转移逻辑 + int res = Math.min( + dp(grid, i, j + 1), + dp(grid, i + 1, j) + ) - grid[i][j]; + // 骑士的生命值至少为 1 + memo[i][j] = res <= 0 ? 1 : res; + + return memo[i][j]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var calculateMinimumHP = function(grid) { + const m = grid.length; + const n = grid[0].length; + // 备忘录中都初始化为 -1 + const memo = new Array(m).fill().map(() => new Array(n).fill(-1)); + + function dp(i, j) { + // base case + if (i === m - 1 && j === n - 1) { + return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; + } + if (i === m || j === n) { + return Number.MAX_VALUE; + } + // 避免重复计算 + if (memo[i][j] !== -1) { + return memo[i][j]; + } + // 状态转移逻辑 + const res = Math.min(dp(i, j + 1), dp(i + 1, j)) - grid[i][j]; + // 骑士的生命值至少为 1 + memo[i][j] = res <= 0 ? 1 : res; + + return memo[i][j]; + } + + return dp(0, 0); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def calculateMinimumHP(self, grid: List[List[int]]) -> int: + m, n = len(grid), len(grid[0]) + # 备忘录中都初始化为 -1 + memo = [[-1 for _ in range(n)] for _ in range(m)] + + return self.dp(grid, 0, 0, memo) + + def dp(self, grid: List[List[int]], i: int, j: int, memo: List[List[int]]) -> int: + m, n = len(grid), len(grid[0]) + # base case + if i == m - 1 and j == n - 1: + return 1 if grid[i][j] >= 0 else -grid[i][j] + 1 + if i == m or j == n: + return float('inf') + # 避免重复计算 + if memo[i][j] != -1: + return memo[i][j] + # 状态转移逻辑 + res = min( + self.dp(grid, i, j + 1, memo), + self.dp(grid, i + 1, j, memo) + ) - grid[i][j] + # 骑士的生命值至少为 1 + memo[i][j] = 1 if res <= 0 else res + + return memo[i][j] +``` + +https://leetcode.cn/problems/dungeon-game 的多语言解法👆 + +https://leetcode.cn/problems/edit-distance 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minDistance(string word1, string word2) { + int m = word1.length(), n = word2.length(); + vector> dp(m + 1, vector(n + 1)); + // base case + for (int i = 1; i <= m; i++) + dp[i][0] = i; + for (int j = 1; j <= n; j++) + dp[0][j] = j; + // 自底向上求解 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1[i - 1] == word2[j - 1]) + dp[i][j] = dp[i - 1][j - 1]; + else + dp[i][j] = min( + dp[i - 1][j] + 1, + /** + ![](../pictures/editDistance/delete.gif) + */ + dp[i][j - 1] + 1, + /** + ![](../pictures/editDistance/insert.gif) + */ + dp[i - 1][j - 1] + 1 + /** + ![](../pictures/editDistance/replace.gif) + */ + ); + } + } + // 储存着整个 s1 和 s2 的最小编辑距离 + return dp[m][n]; + } + + int min(int a, int b, int c) { + return std::min(a, std::min(b, c)); + } +}; +``` + +```go +// by chatGPT (go) +func minDistance(s1 string, s2 string) int { + m, n := len(s1), len(s2) + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + // base case + for i := 1; i <= m; i++ { + dp[i][0] = i + } + for j := 1; j <= n; j++ { + dp[0][j] = j + } + // 自底向上求解 + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if s1[i-1] == s2[j-1] { + dp[i][j] = dp[i-1][j-1] + } else { + dp[i][j] = min( + dp[i-1][j]+1, + /** + ![](../pictures/editDistance/delete.gif) + */ + dp[i][j-1]+1, + /** + ![](../pictures/editDistance/insert.gif) + */ + dp[i-1][j-1]+1, + /** + ![](../pictures/editDistance/replace.gif) + */ + ) + } + } + } + // 储存着整个 s1 和 s2 的最小编辑距离 + return dp[m][n] +} + +func min(a, b, c int) int { + if a < b { + if a < c { + return a + } + return c + } else if b < c { + return b + } + return c +} +``` + +```java +// by labuladong (java) +class Solution { + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + int[][] dp = new int[m + 1][n + 1]; + // base case + for (int i = 1; i <= m; i++) + dp[i][0] = i; + for (int j = 1; j <= n; j++) + dp[0][j] = j; + // 自底向上求解 + for (int i = 1; i <= m; i++) + for (int j = 1; j <= n; j++) + if (s1.charAt(i - 1) == s2.charAt(j - 1)) + dp[i][j] = dp[i - 1][j - 1]; + else + dp[i][j] = min( + dp[i - 1][j] + 1, + /** + ![](../pictures/editDistance/delete.gif) + */ + dp[i][j - 1] + 1, + /** + ![](../pictures/editDistance/insert.gif) + */ + dp[i - 1][j - 1] + 1 + /** + ![](../pictures/editDistance/replace.gif) + */ + ); + // 储存着整个 s1 和 s2 的最小编辑距离 + return dp[m][n]; + } + + int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } +} +``` + +```javascript +// by chatGPT (javascript) +var minDistance = function(s1, s2) { + let m = s1.length, n = s2.length; + let dp = new Array(m + 1).fill().map(() => new Array(n + 1).fill(0)); + // base case + for (let i = 1; i <= m; i++) + dp[i][0] = i; + for (let j = 1; j <= n; j++) + dp[0][j] = j; + // 自底向上求解 + for (let i = 1; i <= m; i++) + for (let j = 1; j <= n; j++) + if (s1.charAt(i - 1) == s2.charAt(j - 1)) + dp[i][j] = dp[i - 1][j - 1]; + else + dp[i][j] = Math.min( + dp[i - 1][j] + 1, + /** + ![](../pictures/editDistance/delete.gif) + */ + dp[i][j - 1] + 1, + /** + ![](../pictures/editDistance/insert.gif) + */ + dp[i - 1][j - 1] + 1 + /** + ![](../pictures/editDistance/replace.gif) + */ + ); + // 储存着整个 s1 和 s2 的最小编辑距离 + return dp[m][n]; +} + +function min(a, b, c) { + return Math.min(a, Math.min(b, c)); +} +``` + +```python +# by chatGPT (python) +class Solution: + def minDistance(self, word1: str, word2: str) -> int: + m, n = len(word1), len(word2) + dp = [[0] * (n+1) for _ in range(m+1)] + + # base case + for i in range(1, m+1): + dp[i][0] = i + for j in range(1, n+1): + dp[0][j] = j + + # 自底向上求解 + for i in range(1, m+1): + for j in range(1, n+1): + if word1[i-1] == word2[j-1]: + dp[i][j] = dp[i-1][j-1] + else: + dp[i][j] = min( + dp[i-1][j] + 1, # insert + dp[i][j-1] + 1, # delete + dp[i-1][j-1] + 1 # replace + ) + + # 储存着整个 word1 和 word2 的最小编辑距离 + return dp[m][n] +``` + +https://leetcode.cn/problems/edit-distance 的多语言解法👆 + +https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 「遍历」的思路 +class Solution { +public: + // 二叉树遍历函数 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + + /**** 前序位置 ****/ + // 每一个节点需要做的事就是交换它的左右子节点 + TreeNode* tmp = root->left; + root->left = root->right; + root->right = tmp; + + // 遍历框架,去遍历左右子树的节点 + traverse(root->left); + traverse(root->right); + } + + // 主函数 + TreeNode* invertTree(TreeNode* root) { + // 遍历二叉树,交换每个节点的子节点 + traverse(root); + return root; + } +}; + +// 「分解问题」的思路 +class Solution2 { +public: + // 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + TreeNode* invertTree(TreeNode* root) { + if (root == nullptr) { + return nullptr; + } + // 利用函数定义,先翻转左右子树 + TreeNode* left = invertTree(root->left); + TreeNode* right = invertTree(root->right); + + // 然后交换左右子节点 + root->left = right; + root->right = left; + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +//「遍历」的思路 +func invertTree(root *TreeNode) *TreeNode { + // 遍历二叉树,交换每个节点的子节点 + traverse(root) + return root +} + +// 二叉树遍历函数 +func traverse(root *TreeNode) { + if root == nil { + return + } + //每一个节点需要做的事就是交换它的左右子节点 + tmp := root.Left + root.Left = root.Right + root.Right = tmp + + // 遍历框架,去遍历左右子树的节点 + traverse(root.Left) + traverse(root.Right) +} + +//「分解问题」的思路 +func invertTree(root *TreeNode) *TreeNode { + if root == nil { + return nil + } + // 利用函数定义,先翻转左右子树 + left := invertTree(root.Left) + right := invertTree(root.Right) + + // 然后交换左右子节点 + root.Left = right + root.Right = left + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root +} +``` + +```java +// by labuladong (java) +// 「遍历」的思路 +class Solution { + // 主函数 + public TreeNode invertTree(TreeNode root) { + // 遍历二叉树,交换每个节点的子节点 + traverse(root); + return root; + } + + // 二叉树遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + + /**** 前序位置 ****/ + // 每一个节点需要做的事就是交换它的左右子节点 + TreeNode tmp = root.left; + root.left = root.right; + root.right = tmp; + + // 遍历框架,去遍历左右子树的节点 + traverse(root.left); + traverse(root.right); + } +} + +// 「分解问题」的思路 +class Solution2 { + // 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + TreeNode invertTree(TreeNode root) { + if (root == null) { + return null; + } + // 利用函数定义,先翻转左右子树 + TreeNode left = invertTree(root.left); + TreeNode right = invertTree(root.right); + + // 然后交换左右子节点 + root.left = right; + root.right = left; + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +// 「遍历」的思路 +var Solution = function() {}; + +// 主函数 +Solution.prototype.invertTree = function(root) { + // 遍历二叉树,交换每个节点的子节点 + this.traverse(root); + return root; +}; + +// 二叉树遍历函数 +Solution.prototype.traverse = function(root) { + if (root == null) { + return; + } + + /**** 前序位置 ****/ + // 每一个节点需要做的事就是交换它的左右子节点 + var tmp = root.left; + root.left = root.right; + root.right = tmp; + + // 遍历框架,去遍历左右子树的节点 + this.traverse(root.left); + this.traverse(root.right); +}; + +// 「分解问题」的思路 +var Solution2 = function() {}; + +// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 +Solution2.prototype.invertTree = function(root) { + if (root == null) { + return null; + } + // 利用函数定义,先翻转左右子树 + var left = this.invertTree(root.left); + var right = this.invertTree(root.right); + + // 然后交换左右子节点 + root.left = right; + root.right = left; + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root; +}; +``` + +```python +# by chatGPT (python) +# 「遍历」的思路 +class Solution: + # 主函数 + def invertTree(self, root: TreeNode) -> TreeNode: + # 遍历二叉树,交换每个节点的子节点 + self.traverse(root) + return root + + # 二叉树遍历函数 + def traverse(self, root: TreeNode) -> None: + if root is None: + return + + # 前序位置 + # 每一个节点需要做的事就是交换它的左右子节点 + tmp = root.left + root.left = root.right + root.right = tmp + + # 遍历框架,去遍历左右子树的节点 + self.traverse(root.left) + self.traverse(root.right) + + +# 「分解问题」的思路 +class Solution2: + # 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + def invertTree(self, root: TreeNode) -> TreeNode: + if root is None: + return None + + # 利用函数定义,先翻转左右子树 + left = self.invertTree(root.left) + right = self.invertTree(root.right) + + # 然后交换左右子节点 + root.left = right + root.right = left + + # 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root +``` + +https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof 的多语言解法👆 + +https://leetcode.cn/problems/er-cha-shu-de-shen-du-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +/***** 解法一,回溯算法思路 *****/ +class Solution { + int depth = 0; + int res = 0; + +public: + int maxDepth(TreeNode* root) { + traverse(root); + return res; + } + + // 遍历二叉树 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + + // 前序遍历位置 + depth++; + // 遍历的过程中记录最大深度 + res = std::max(res, depth); + traverse(root->left); + traverse(root->right); + // 后序遍历位置 + depth--; + } +}; + +/***** 解法二,动态规划思路 *****/ +class Solution2 { +public: + // 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 + int maxDepth(TreeNode* root) { + if (root == nullptr) { + return 0; + } + int leftMax = maxDepth(root->left); + int rightMax = maxDepth(root->right); + // 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + std::max(leftMax, rightMax); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + + // 解法一,回溯算法思路 +func maxDepth(root *TreeNode) int { + depth := 0 + res := 0 + var traverse func(root *TreeNode) + + traverse = func(root *TreeNode) { + if root == nil { + return + } + + depth++ + res = Max(res, depth) + traverse(root.Left) + traverse(root.Right) + depth-- + } + + traverse(root) + return res +} + +// 解法二,动态规划思路 +func maxDepth2(root *TreeNode) int { + if root == nil { + return 0 + } + leftMax := maxDepth(root.Left) + rightMax := maxDepth(root.Right) + return 1 + Max(leftMax, rightMax) +} + +func Max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +/***** 解法一,回溯算法思路 *****/ +class Solution { + + int depth = 0; + int res = 0; + + public int maxDepth(TreeNode root) { + traverse(root); + return res; + } + + // 遍历二叉树 + void traverse(TreeNode root) { + if (root == null) { + return; + } + + // 前序遍历位置 + depth++; + // 遍历的过程中记录最大深度 + res = Math.max(res, depth); + traverse(root.left); + traverse(root.right); + // 后序遍历位置 + depth--; + } +} + +/***** 解法二,动态规划思路 *****/ +class Solution2 { + // 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 + public int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + // 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + Math.max(leftMax, rightMax); + } +} +``` + +```javascript +// by chatGPT (javascript) +// 解法一,回溯算法思路 +var maxDepth = function(root) { + let depth = 0; + let res = 0; + + // 遍历二叉树 + function traverse(root) { + if (root === null) { + return; + } + + // 前序遍历位置 + depth++; + // 遍历的过程中记录最大深度 + res = Math.max(res, depth); + traverse(root.left); + traverse(root.right); + // 后序遍历位置 + depth--; + } + + traverse(root); + return res; +}; + +// 解法二,动态规划思路 +var maxDepth = function(root) { + // 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 + if (root === null) { + return 0; + } + let leftMax = maxDepth(root.left); + let rightMax = maxDepth(root.right); + // 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + Math.max(leftMax, rightMax); +}; +``` + +```python +# by chatGPT (python) +# 解法一,回溯算法思路 +class Solution: + def maxDepth(self, root: TreeNode) -> int: + depth, res = 0, 0 + self.traverse(root, depth, res) + return res + + # 遍历二叉树 + def traverse(self, root: TreeNode, depth: int, res: int) -> None: + if not root: + return + # 前序遍历位置 + depth += 1 + # 遍历的过程中记录最大深度 + res = max(res, depth) + self.traverse(root.left, depth, res) + self.traverse(root.right, depth, res) + # 后序遍历位置 + depth -= 1 + +# 解法二,动态规划思路 +class Solution2: + # 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 + def maxDepth(self, root: TreeNode) -> int: + if not root: + return 0 + leftMax = self.maxDepth(root.left) + rightMax = self.maxDepth(root.right) + # 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + max(leftMax, rightMax) +``` + +https://leetcode.cn/problems/er-cha-shu-de-shen-du-lcof 的多语言解法👆 + +https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + // base case + if (root == nullptr) return nullptr; + if (root == p || root == q) return root; + + TreeNode* left = lowestCommonAncestor(root->left, p, q); + TreeNode* right = lowestCommonAncestor(root->right, p, q); + // 情况 1 + if (left != nullptr && right != nullptr) { + return root; + } + // 情况 2 + if (left == nullptr && right == nullptr) { + return nullptr; + } + // 情况 3 + return left == nullptr ? right : left; + } +}; +``` + +```go +// by chatGPT (go) +func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { + // base case + if root == nil { + return nil + } + if root == p || root == q { + return root + } + + left := lowestCommonAncestor(root.Left, p, q) + right := lowestCommonAncestor(root.Right, p, q) + // 情况 1 + if left != nil && right != nil { + return root + } + // 情况 2 + if left == nil && right == nil { + return nil + } + // 情况 3 + if left == nil { + return right + } else { + return left + } +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + // base case + if (root == null) return null; + if (root == p || root == q) return root; + + TreeNode left = lowestCommonAncestor(root.left, p, q); + TreeNode right = lowestCommonAncestor(root.right, p, q); + // 情况 1 + if (left != null && right != null) { + return root; + } + // 情况 2 + if (left == null && right == null) { + return null; + } + // 情况 3 + return left == null ? right : left; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @param {TreeNode} p + * @param {TreeNode} q + * @return {TreeNode} + */ +var lowestCommonAncestor = function(root, p, q) { + // base case + if (root == null) return null; + if (root == p || root == q) return root; + + var left = lowestCommonAncestor(root.left, p, q); + var right = lowestCommonAncestor(root.right, p, q); + // 情况 1 + if (left != null && right != null) { + return root; + } + // 情况 2 + if (left == null && right == null) { + return null; + } + // 情况 3 + return left == null ? right : left; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': + # base case + if not root: + return None + if root == p or root == q: + return root + + left = self.lowestCommonAncestor(root.left, p, q) + right = self.lowestCommonAncestor(root.right, p, q) + + # 情况 1 + if left and right: + return root + + # 情况 2 + if not left and not right: + return None + + # 情况 3 + return right if not left else left +``` + +https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof 的多语言解法👆 + +https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int kthLargest(TreeNode* root, int k) { + // 利用 BST 的中序遍历特性 + traverse(root, k); + return res; + } + + // 记录结果 + int res = 0; + // 记录当前元素的排名 + int rank = 0; + void traverse(TreeNode* root, int k) { + if (root == nullptr) { + return; + } + traverse(root->right, k); + /* 中序遍历代码位置 */ + rank++; + if (k == rank) { + // 找到第 k 大的元素 + res = root->val; + return; + } + /*****************/ + traverse(root->left, k); + } +}; +``` + +```go +// by chatGPT (go) +func kthLargest(root *TreeNode, k int) int { + // 利用 BST 的中序遍历特性 + traverse := func(node *TreeNode, k int) {} + res, rank := 0, 0 + + traverse = func(node *TreeNode, k int) { + if node == nil { + return + } + traverse(node.Right, k) + /* 中序遍历代码位置 */ + rank++ + if k == rank { + // 找到第 k 大的元素 + res = node.Val + return + } + /*****************/ + traverse(node.Left, k) + } + + traverse(root, k) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int kthLargest(TreeNode root, int k) { + // 利用 BST 的中序遍历特性 + traverse(root, k); + return res; + } + + // 记录结果 + int res = 0; + // 记录当前元素的排名 + int rank = 0; + void traverse(TreeNode root, int k) { + if (root == null) { + return; + } + traverse(root.right, k); + /* 中序遍历代码位置 */ + rank++; + if (k == rank) { + // 找到第 k 大的元素 + res = root.val; + return; + } + /*****************/ + traverse(root.left, k); + } +} +``` + +```javascript +// by chatGPT (javascript) +var kthLargest = function(root, k) { + let res = 0, rank = 0; + + const traverse = function(root, k) { + if (root === null) { + return; + } + traverse(root.right, k); + /* 中序遍历代码位置 */ + rank++; + if (k === rank) { + // 找到第 k 大的元素 + res = root.val; + return; + } + /*****************/ + traverse(root.left, k); + } + + traverse(root, k); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def kthLargest(self, root: TreeNode, k: int) -> int: + # 利用 BST 的中序遍历特性 + self.rank = 0 + self.traverse(root, k) + return self.res + + # 记录结果 + res = 0 + # 记录当前元素的排名 + rank = 0 + + def traverse(self, root: TreeNode, k: int) -> None: + if not root: + return + + self.traverse(root.right, k) + """ 中序遍历代码位置 """ + self.rank += 1 + if self.rank == k: + # 找到第 k 大的元素 + self.res = root.val + return + /*****************/ + self.traverse(root.left, k) +``` + +https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof 的多语言解法👆 + +https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool verifyPostorder(vector& postorder) { + return check(postorder, 0, postorder.size() - 1); + } + + // 定义:检查 postorder[i..j] 是否是一个合法的 BST + bool check(vector& postorder, int i, int j) { + if (i >= j) { + return true; + } + // 根节点的值是后序遍历结果的最后一个元素 + int root = postorder[j]; + + // postorder[i..left) 是左子树,应该都小于 root + int left = i; + while (left < j && postorder[left] < root) { + left++; + } + + // postorder[left..j) 是右子树,应该都大于 root + int right = left; + while (right < j && postorder[right] > root) { + right++; + } + if (right != j) { + return false; + } + // 递归检查左子树 [i..left) 和右子树 [left..j) 也符合 BST 的性质 + return check(postorder, i, left - 1) && check(postorder, left, j - 1); + } +}; +``` + +```go +// by chatGPT (go) +func verifyPostorder(postorder []int) bool { + return check(postorder, 0, len(postorder) - 1) +} + +// 定义:检查 postorder[i..j] 是否是一个合法的 BST +func check(postorder []int, i int, j int) bool { + if i >= j { + return true + } + // 根节点的值是后序遍历结果的最后一个元素 + root := postorder[j] + /** + ![](../pictures/二叉树系列2/5.jpeg) + */ + + // postorder[i..left) 是左子树,应该都小于 root + left := i + for left < j && postorder[left] < root { + left++ + } + + // postorder[left..j) 是右子树,应该都大于 root + right := left + for right < j && postorder[right] > root { + right++ + } + if right != j { + return false + } + // 递归检查左子树 [i..left) 和右子树 [left..j) 也符合 BST 的性质 + return check(postorder, i, left - 1) && check(postorder, left, j - 1) +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean verifyPostorder(int[] postorder) { + return check(postorder, 0, postorder.length - 1); + } + + // 定义:检查 postorder[i..j] 是否是一个合法的 BST + boolean check(int[] postorder, int i, int j) { + if (i >= j) { + return true; + } + // 根节点的值是后序遍历结果的最后一个元素 + int root = postorder[j]; + /** + ![](../pictures/二叉树系列2/5.jpeg) + */ + + // postorder[i..left) 是左子树,应该都小于 root + int left = i; + while (left < j && postorder[left] < root) { + left++; + } + + // postorder[left..j) 是右子树,应该都大于 root + int right = left; + while (right < j && postorder[right] > root) { + right++; + } + if (right != j) { + return false; + } + // 递归检查左子树 [i..left) 和右子树 [left..j) 也符合 BST 的性质 + return check(postorder, i, left - 1) && check(postorder, left, j - 1); + } + +} +``` + +```javascript +// by chatGPT (javascript) +var verifyPostorder = function(postorder) { + return check(postorder, 0, postorder.length - 1); +}; + +// 定义:检查 postorder[i..j] 是否是一个合法的 BST +function check(postorder, i, j) { + if (i >= j) { + return true; + } + // 根节点的值是后序遍历结果的最后一个元素 + let root = postorder[j]; + + // postorder[i..left) 是左子树,应该都小于 root + let left = i; + while (left < j && postorder[left] < root) { + left++; + } + + // postorder[left..j) 是右子树,应该都大于 root + let right = left; + while (right < j && postorder[right] > root) { + right++; + } + if (right != j) { + return false; + } + // 递归检查左子树 [i..left) 和右子树 [left..j) 也符合 BST 的性质 + return check(postorder, i, left - 1) && check(postorder, left, j - 1); +} +``` + +```python +# by chatGPT (python) +class Solution: + def verifyPostorder(self, postorder: List[int]) -> bool: + return self.check(postorder, 0, len(postorder)-1) + + def check(self, postorder: List[int], i: int, j: int) -> bool: + if i >= j: + return True + # 根节点的值是后序遍历结果的最后一个元素 + root = postorder[j] + + # postorder[i..left) 是左子树,应该都小于 root + left = i + while left < j and postorder[left] < root: + left += 1 + + # postorder[left..j) 是右子树,应该都大于 root + right = left + while right < j and postorder[right] > root: + right += 1 + + if right != j: + return False + + # 递归检查左子树 [i..left) 和右子树 [left..j) 也符合 BST 的性质 + return self.check(postorder, i, left-1) and self.check(postorder, left, j-1) +``` + +https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof 的多语言解法👆 + +https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int hammingWeight(uint32_t n) { + int res = 0; + while (n != 0) { + n = n & (n - 1); + res++; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func hammingWeight(num uint32) int { + res := 0 + for num != 0 { + num &= (num - 1) + res++ + } + return res +} +``` + +```java +// by labuladong (java) +public class Solution { + // you need to treat n as an unsigned value + public int hammingWeight(int n) { + int res = 0; + while (n != 0) { + n = n & (n - 1); + res++; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n - a positive integer + * @return {number} - the number of bits set to 1 in the binary representation of n + */ +var hammingWeight = function(n) { + let res = 0; + while (n !== 0) { + n = n & (n - 1); + res++; + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def hammingWeight(self, n: int) -> int: + """ + :type n: int + :rtype: int + """ + res = 0 + while n != 0: + n &= n - 1 + res += 1 + return res +``` + +https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool searchMatrix(vector>& matrix, int target) { + int m = matrix.size(), n = matrix[0].size(); + // 初始化在右上角 + int i = 0, j = n - 1; + while (i < m && j >= 0) { + if (matrix[i][j] == target) { + return true; + } + if (matrix[i][j] < target) { + // 需要大一点,往下移动 + i++; + } else { + // 需要小一点,往左移动 + j--; + } + } + // while 循环中没有找到,则 target 不存在 + return false; + } +}; +``` + +```go +// by chatGPT (go) +func searchMatrix(matrix [][]int, target int) bool { + m, n := len(matrix), len(matrix[0]) + // 初始化在右上角 + i, j := 0, n-1 + for i < m && j >= 0 { + if matrix[i][j] == target { + return true + } + if matrix[i][j] < target { + // 需要大一点,往下移动 + i++ + } else { + // 需要小一点,往左移动 + j-- + } + } + // while 循环中没有找到,则 target 不存在 + return false +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean searchMatrix(int[][] matrix, int target) { + int m = matrix.length, n = matrix[0].length; + // 初始化在右上角 + int i = 0, j = n - 1; + while (i < m && j >= 0) { + if (matrix[i][j] == target) { + return true; + } + if (matrix[i][j] < target) { + // 需要大一点,往下移动 + i++; + } else { + // 需要小一点,往左移动 + j--; + } + } + // while 循环中没有找到,则 target 不存在 + return false; + } +} +``` + +```javascript +// by chatGPT (javascript) +var searchMatrix = function(matrix, target) { + const m = matrix.length; + const n = matrix[0].length; + // 初始化在右上角 + let i = 0; + let j = n - 1; + while (i < m && j >= 0) { + if (matrix[i][j] === target) { + return true; + } + if (matrix[i][j] < target) { + // 需要大一点,往下移动 + i++; + } else { + // 需要小一点,往左移动 + j--; + } + } + // while 循环中没有找到,则 target 不存在 + return false; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def searchMatrix(self, matrix: List[List[int]], target: int) -> bool: + m, n = len(matrix), len(matrix[0]) + # 初始化在右上角 + i, j = 0, n - 1 + while i < m and j >= 0: + if matrix[i][j] == target: + return True + if matrix[i][j] < target: + # 需要大一点,往下移动 + i += 1 + else: + # 需要小一点,往左移动 + j -= 1 + # while 循环中没有找到,则 target 不存在 + return False +``` + +https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof 的多语言解法👆 + +https://leetcode.cn/problems/evaluate-reverse-polish-notation 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int evalRPN(vector& tokens) { + stack stk; + for (const string& token : tokens) { + if (string("+-*/").find(token) != string::npos) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + int a = stk.top(); stk.pop(); + int b = stk.top(); stk.pop(); + switch (token[0]) { + case '+': + stk.push(a + b); + break; + case '*': + stk.push(a * b); + break; + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case '-': + stk.push(b - a); + break; + case '/': + stk.push(b / a); + break; + } + } else { + // 是个数字,直接入栈即可 + stk.push(stoi(token)); + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk.top(); + } +}; +``` + +```go +// by chatGPT (go) +func evalRPN(tokens []string) int { + stk := make([]int, 0) + for _, token := range tokens { + if strings.Contains("+-*/", token) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + a, b := stk[len(stk)-1], stk[len(stk)-2] + stk = stk[:len(stk)-2] + switch token { + case "+": + stk = append(stk, b+a) + case "*": + stk = append(stk, b*a) + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case "-": + stk = append(stk, b-a) + case "/": + stk = append(stk, b/a) + } + } else { + // 是个数字,直接入栈即可 + num, _ := strconv.Atoi(token) + stk = append(stk, num) + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk[0] +} +``` + +```java +// by labuladong (java) +class Solution { + public int evalRPN(String[] tokens) { + Stack stk = new Stack<>(); + for (String token : tokens) { + if ("+-*/".contains(token)) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + int a = stk.pop(), b = stk.pop(); + switch (token) { + case "+": + stk.push(a + b); + break; + case "*": + stk.push(a * b); + break; + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case "-": + stk.push(b - a); + break; + case "/": + stk.push(b / a); + break; + } + } else { + // 是个数字,直接入栈即可 + stk.push(Integer.parseInt(token)); + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk.pop(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string[]} tokens + * @return {number} + */ +var evalRPN = function(tokens) { + const stk = []; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if ("+-*/".indexOf(token) !== -1) { + // 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + const a = stk.pop(), b = stk.pop(); + switch (token) { + case "+": + stk.push(a + b); + break; + case "*": + stk.push(a * b); + break; + // 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + case "-": + stk.push(b - a); + break; + case "/": + stk.push(b / a | 0); // 取整 + break; + } + } else { + // 是个数字,直接入栈即可 + stk.push(parseInt(token)); + } + } + // 最后栈中剩下一个数字,即是计算结果 + return stk.pop(); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def evalRPN(self, tokens: List[str]) -> int: + stk = [] + for token in tokens: + if token in "+-*/": + # 是个运算符,从栈顶拿出两个数字进行运算,运算结果入栈 + a, b = stk.pop(), stk.pop() + if token == "+": + stk.append(a + b) + elif token == "*": + stk.append(a * b) + # 对于减法和除法,顺序别搞反了,第二个数是被除(减)数 + elif token == "-": + stk.append(b - a) + else: + stk.append(int(b / a)) + else: + # 是个数字,直接入栈即可 + stk.append(int(token)) + # 最后栈中剩下一个数字,即是计算结果 + return stk.pop() +``` + +https://leetcode.cn/problems/evaluate-reverse-polish-notation 的多语言解法👆 + +https://leetcode.cn/problems/exam-room 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class ExamRoom { +private: + // 将端点 p 映射到以 p 为左端点的线段 + unordered_map> startMap; + // 将端点 p 映射到以 p 为右端点的线段 + unordered_map> endMap; + // 根据线段长度从小到大存放所有线段 + set> pq; + int N; +public: + ExamRoom(int N) { + this->N = N; + startMap.clear(); + endMap.clear(); + pq.clear(); + pq.insert({-1, N}); + } + + int seat() { + // 从有序集合拿出最长的线段 + auto longest = prev(pq.end()); + int x = longest->at(0); + int y = longest->at(1); + int seat; + if (x == -1) { // 情况一 + seat = 0; + } else if (y == N) { // 情况二 + seat = N - 1; + } else { // 情况三 + seat = (y - x) / 2 + x; + } + // 将最长的线段分成两段 + auto left = vector{x, seat}; + auto right = vector{seat, y}; + removeInterval(*longest); + addInterval(left); + addInterval(right); + return seat; + } + + void leave(int p) { + // 将 p 左右的线段找出来 + auto right = startMap[p]; + auto left = endMap[p]; + // 合并两个线段成为一个线段 + auto merged = vector{left[0], right[1]}; + removeInterval(left); + removeInterval(right); + addInterval(merged); + } + + + /* 增加一个线段 */ + void addInterval(vector intv) { + pq.insert(intv); + startMap[intv[0]] = intv; + endMap[intv[1]] = intv; + } + + /* 去除一个线段 */ + void removeInterval(vector intv) { + pq.erase(intv); + startMap.erase(intv[0]); + endMap.erase(intv[1]); + } + + /* 计算一个线段的长度 */ + int distance(vector intv) { + int x = intv[0]; + int y = intv[1]; + if (x == -1) return y; + if (y == N) return N - 1 - x; + // 中点和端点之间的长度 + return (y - x) / 2; + } +}; +``` + +```go +// by chatGPT (go) +type ExamRoom struct { + // 将端点 p 映射到以 p 为左端点的线段 + startMap map[int][]int + // 将端点 p 映射到以 p 为右端点的线段 + endMap map[int][]int + // 根据线段长度从小到大存放所有线段 + pq TreeSet + N int +} + +func Constructor(N int) ExamRoom { + s := make(map[int][]int) + e := make(map[int][]int) + pq := newSTreeSet(func(a, b []int) int { + distA := distance(a) + distB := distance(b) + if distA == distB { + return b[0] - a[0] + } + return distA - distB + }) + addInterval := func(intv []int) { + pq.add(intv) + s[intv[0]] = intv + e[intv[1]] = intv + } + removeInterval := func(intv []int) { + pq.remove(intv) + delete(s, intv[0]) + delete(e, intv[1]) + } + + addInterval([]int{-1, N}) + return ExamRoom{startMap: s, endMap: e, pq: pq, N: N} +} + +func (this *ExamRoom) Seat() int { + longest := this.pq.last() + x, y := longest[0], longest[1] + var seat int + if x == -1 { // 情况一 + seat = 0 + } else if y == this.N { // 情况二 + seat = this.N - 1 + } else { // 情况三 + seat = (y - x) / 2 + x + } + left := []int{x, seat} + right := []int{seat, y} + removeInterval := func() { + this.pq.remove(longest) + delete(this.startMap, longest[0]) + delete(this.endMap, longest[1]) + } + removeInterval() + addInterval(left) + addInterval(right) + return seat +} + +func (this *ExamRoom) Leave(p int) { + right, left := this.startMap[p], this.endMap[p] + merged := []int{left[0], right[1]} + removeInterval := func(intv []int) { + this.pq.remove(intv) + delete(this.startMap, intv[0]) + delete(this.endMap, intv[1]) + } + removeInterval(left) + removeInterval(right) + addInterval(merged) +} + +/* 计算一个线段的长度 */ +func distance(intv []int) int { + x, y := intv[0], intv[1] + if x == -1 { + return y + } + if y == N { + return N - 1 - x + } + return (y - x) / 2 +} + +type lambdaHeap struct { + less func(i, j []int) bool + pq [][]int +} + +func (h lambdaHeap) Len() int { return len(h.pq) } +func (h lambdaHeap) Less(i, j int) bool { + return h.less(h.pq[i], h.pq[j]) +} +func (h lambdaHeap) Swap(i, j int) { + h.pq[i], h.pq[j] = h.pq[j], h.pq[i] +} +func (h *lambdaHeap) Push(x interface{}) { + h.pq = append(h.pq, x.([]int)) +} +func (h *lambdaHeap) Pop() interface{} { + n := len(h.pq) + x := h.pq[n-1] + h.pq = h.pq[:n-1] + return x +} + +type TreeSet struct { + less func(i, j []int) int + heap lambdaHeap +} + +func newSTreeSet(f func(i, j []int) int) TreeSet { + h := lambdaHeap{less: func(i, j []int) bool { + return f(i, j) < 0 + }} + return TreeSet{less: f, heap: h} +} + +func (t TreeSet) resize(n int) { + pq := lambdaHeap{less: t.heap.less, pq: make([][]int, n)} + copy(pq.pq, t.heap.pq) + t.heap = pq +} + +func (t *TreeSet) add(x []int) { + t.heap.Push(x) + if len(t.heap.pq) > (cap(t.heap.pq) >> 1) { + t.resize(len(t.heap.pq) << 1) + } + for i, j := len(t.heap.pq)-1, (len(t.heap.pq)-2)>>1; i > 0 && t.heap.Less(i, j); i, j = j, (j-1)>>1 { + t.heap.Swap(i, j) + } +} + +func (t *TreeSet) find(x []int) int { + l, r := 0, len(t.heap.pq)-1 + less := t.less(x, t.heap.pq[0]) + if less < 0 { + return -1 + } else if less == 0 { + return 0 + } + for l < r { + mid := (l + r) >> 1 + if t.less(x, t.heap.pq[mid]) < 0 { + r = mid + } else { + l = mid + 1 + } + } + return l +} + +func (t *TreeSet) remove(x []int) { + i := t.find(x) + if i == -1 { + return + } + t.heap.Swap(i, len(t.heap.pq)-1) + t.heap.pq = t.heap.pq[:len(t.heap.pq)-1] + for i < len(t.heap.pq) { + left, right := (i<<1)|1, (i+1)<<1 + if left >= len(t.heap.pq) { + break + } + k := left + if right < len(t.heap.pq) && t.heap.Less(right, left) { + k = right + } + if !t.heap.Less(k, i) { + break + } + t.heap.Swap(i, k) + i = k + } +} +``` + +```java +// by labuladong (java) +class ExamRoom { + // 将端点 p 映射到以 p 为左端点的线段 + private Map startMap; + // 将端点 p 映射到以 p 为右端点的线段 + private Map endMap; + // 根据线段长度从小到大存放所有线段 + private TreeSet pq; + private int N; + + public ExamRoom(int N) { + this.N = N; + startMap = new HashMap<>(); + endMap = new HashMap<>(); + pq = new TreeSet<>((a, b) -> { + int distA = distance(a); + int distB = distance(b); + // 如果长度相同,就比较索引 + if (distA == distB) + return b[0] - a[0]; + return distA - distB; + }); + // 在有序集合中先放一个虚拟线段 + addInterval(new int[]{-1, N}); + } + + public int seat() { + // 从有序集合拿出最长的线段 + int[] longest = pq.last(); + int x = longest[0]; + int y = longest[1]; + int seat; + if (x == -1) { // 情况一 + seat = 0; + } else if (y == N) { // 情况二 + seat = N - 1; + } else { // 情况三 + seat = (y - x) / 2 + x; + } + // 将最长的线段分成两段 + int[] left = new int[]{x, seat}; + int[] right = new int[]{seat, y}; + removeInterval(longest); + addInterval(left); + addInterval(right); + return seat; + } + + public void leave(int p) { + // 将 p 左右的线段找出来 + int[] right = startMap.get(p); + int[] left = endMap.get(p); + // 合并两个线段成为一个线段 + int[] merged = new int[]{left[0], right[1]}; + removeInterval(left); + removeInterval(right); + addInterval(merged); + } + + + /* 增加一个线段 */ + private void addInterval(int[] intv) { + pq.add(intv); + startMap.put(intv[0], intv); + endMap.put(intv[1], intv); + } + + /* 去除一个线段 */ + private void removeInterval(int[] intv) { + pq.remove(intv); + startMap.remove(intv[0]); + endMap.remove(intv[1]); + } + + /* 计算一个线段的长度 */ + private int distance(int[] intv) { + int x = intv[0]; + int y = intv[1]; + if (x == -1) return y; + if (y == N) return N - 1 - x; + // 中点和端点之间的长度 + return (y - x) / 2; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} N + */ +var ExamRoom = function(N) { + // 将端点 p 映射到以 p 为左端点的线段 + this.startMap = new Map(); + // 将端点 p 映射到以 p 为右端点的线段 + this.endMap = new Map(); + // 根据线段长度从小到大存放所有线段 + this.pq = new TreeMap((a, b) => { + let distA = this.distance(a); + let distB = this.distance(b); + // 如果长度相同,就比较索引 + if (distA === distB) { + return b[0] - a[0]; + } + return distA - distB; + }); + this.N = N; + // 在有序集合中先放一个虚拟线段 + this.addInterval([-1, N]); +}; + +/** + * @return {number} + */ +ExamRoom.prototype.seat = function() { + // 从有序集合拿出最长的线段 + let longest = this.pq.last(); + let x = longest[0]; + let y = longest[1]; + let seat; + if (x === -1) { // 情况一 + seat = 0; + } else if (y === this.N) { // 情况二 + seat = this.N - 1; + } else { // 情况三 + seat = Math.floor((y - x) / 2) + x; + } + // 将最长的线段分成两段 + let left = [x, seat]; + let right = [seat, y]; + this.removeInterval(longest); + this.addInterval(left); + this.addInterval(right); + return seat; +}; + +/** + * @param {number} p + * @return {void} + */ +ExamRoom.prototype.leave = function(p) { + // 将 p 左右的线段找出来 + let right = this.startMap.get(p); + let left = this.endMap.get(p); + // 合并两个线段成为一个线段 + let merged = [left[0], right[1]]; + this.removeInterval(left); + this.removeInterval(right); + this.addInterval(merged); +}; + +/** + * @param {number[]} intv + * @return {void} + */ +ExamRoom.prototype.addInterval = function(intv) { + this.pq.add(intv); + this.startMap.set(intv[0], intv); + this.endMap.set(intv[1], intv); +}; + +/** + * @param {number[]} intv + * @return {void} + */ +ExamRoom.prototype.removeInterval = function(intv) { + this.pq.remove(intv); + this.startMap.delete(intv[0]); + this.endMap.delete(intv[1]); +}; + +/** + * @param {number[]} intv + * @return {number} + */ +ExamRoom.prototype.distance = function(intv) { + let x = intv[0]; + let y = intv[1]; + if (x === -1) { + return y; + } + if (y === this.N) { + return this.N - 1 - x; + } + // 中点和端点之间的长度 + return Math.floor((y - x) / 2); +}; +``` + +```python +# by chatGPT (python) +class ExamRoom: + def __init__(self, N: int): + self.N = N + self.startMap = {} + self.endMap = {} + self.pq = [(self.distance([-1, self.N]), -1, self.N)] # 一个元组,第一个元素为长度,后两个为坐标 + # 所有线段已按照长度排序,第一个元素为最小长度 + # 表示最开始的虚拟线段 + def seat(self) -> int: + # 从优先队列拿出长度最大的线段 + dist, x, y = heapq.heappop(self.pq) + if x == -1: # 若最大线段为虚拟线段,直接返回 + seat = 0 + elif y == self.N: + seat = self.N - 1 + else: + seat = (y - x) // 2 + x # 计算座位位置 + # 把新线段分成两个加入优先队列 + heapq.heappush(self.pq, (self.distance([x, seat]), x, seat)) + heapq.heappush(self.pq, (self.distance([seat, y]), seat, y)) + # 更新两个映射 + self.startMap[seat] = [x, seat] + self.endMap[seat] = [seat, y] + return seat + + def leave(self, p: int) -> None: + # 从两个映射找到左右线段,并将它们删除 + left, right = self.startMap[p], self.endMap[p] + del self.startMap[p], self.endMap[p] + # 从优先队列中删除两个线段 + self.pq.remove((self.distance(left), left[0], left[1])) + self.pq.remove((self.distance(right), right[0], right[1])) + # 合并左右线段成为一个新的线段 + merged = [left[0], right[1]] + heapq.heappush(self.pq, (self.distance(merged), merged[0], merged[1])) + + def distance(self, interval: List[int]) -> int: + x, y = interval + if x == -1: return y + if y == self.N: return self.N - 1 - x + return abs(y - x) // 2 +``` + +https://leetcode.cn/problems/exam-room 的多语言解法👆 + +https://leetcode.cn/problems/factorial-trailing-zeroes 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int trailingZeroes(int n) { + int res = 0; + long long divisor = 5; + while (divisor <= n) { + res += n / divisor; + divisor *= 5; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func trailingZeroes(n int) int { + res := 0 + divisor := int64(5) + for divisor <= int64(n) { + res += n / int(divisor) + divisor *= 5 + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int trailingZeroes(int n) { + int res = 0; + long divisor = 5; + while (divisor <= n) { + res += n / divisor; + divisor *= 5; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number} + */ +var trailingZeroes = function(n) { + let res = 0; + let divisor = 5; + while (divisor <= n) { + res += Math.floor(n / divisor); + divisor *= 5; + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def trailingZeroes(self, n: int) -> int: + res = 0 + divisor = 5 + while divisor <= n: + res += n // divisor + divisor *= 5 + return res +``` + +https://leetcode.cn/problems/factorial-trailing-zeroes 的多语言解法👆 + +https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string reverseWords(string s) { + // 先清洗一下数据,把多于的空格都删掉 + string sb; + for (int i = 0; i < s.length(); i++) { + char c = s[i]; + if (c != ' ') { + // 单词中的字母/数字 + sb += c; + } else if (!sb.empty() && sb[sb.length() - 1] != ' ') { + // 单词之间保留一个空格 + sb += ' '; + } + } + if (sb.empty()) { + return ""; + } + // 末尾如果有空格,清除之 + if (sb[sb.length() - 1] == ' ') { + sb.erase(sb.length() - 1, 1); + } + + // 清洗之后的字符串 + char* chars = new char[sb.length() + 1]; + strcpy(chars, sb.c_str()); + int n = strlen(chars); + // 进行单词的翻转,先整体翻转 + reverse(chars, 0, n - 1); + // 再把每个单词翻转 + for (int i = 0; i < n; ) { + for (int j = i; j < n; j++) { + if (j + 1 == n || chars[j + 1] == ' ') { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j); + // 把 i 置为下一个单词的首字母 + i = j + 2; + break; + } + } + } + // 最后得到题目想要的结果 + string res(chars); + delete[] chars; + return res; + } + + // 翻转 arr[i..j] + void reverse(char* arr, int i, int j) { + while (i < j) { + char temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +}; +``` + +```go +// by chatGPT (go) +func reverseWords(s string) string { + var sb strings.Builder + // 先清洗一下数据,把多余的空格都删掉 + for i := 0; i < len(s); i++ { + c := s[i] + if c != ' ' { + // 单词中的字母/数字 + sb.WriteByte(c) + } else if sb.Len() > 0 && sb.String()[sb.Len()-1] != ' ' { + // 单词之间保留一个空格 + sb.WriteByte(' ') + } + } + // 末尾如果有空格,清除之 + if sb.Len() > 0 && sb.String()[sb.Len()-1] == ' ' { + sb.Truncate(sb.Len() - 1) + } + + // 清洗之后的字符串 + chars := []byte(sb.String()) + n := len(chars) + // 进行单词的翻转,先整体翻转 + reverse := func(arr []byte, i int, j int) { + for i < j { + arr[i], arr[j] = arr[j], arr[i] + i++ + j-- + } + } + reverse(chars, 0, n-1) + // 再把每个单词翻转 + for i := 0; i < n; { + for j := i; j < n; j++ { + if j+1 == n || chars[j+1] == ' ' { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j) + // 把 i 置为下一个单词的首字母 + i = j + 2 + break + } + } + } + // 最后得到题目想要的结果 + return string(chars) +} +``` + +```java +// by labuladong (java) +class Solution { + public String reverseWords(String s) { + StringBuilder sb = new StringBuilder(); + // 先清洗一下数据,把多于的空格都删掉 + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c != ' ') { + // 单词中的字母/数字 + sb.append(c); + } else if (!sb.isEmpty() && sb.charAt(sb.length() - 1) != ' ') { + // 单词之间保留一个空格 + sb.append(' '); + } + } + if (sb.isEmpty()) { + return ""; + } + // 末尾如果有空格,清除之 + if (sb.charAt(sb.length() - 1) == ' ') { + sb.deleteCharAt(sb.length() - 1); + } + + // 清洗之后的字符串 + char[] chars = sb.toString().toCharArray(); + int n = chars.length; + // 进行单词的翻转,先整体翻转 + reverse(chars, 0, n - 1); + // 再把每个单词翻转 + for (int i = 0; i < n; ) { + for (int j = i; j < n; j++) { + if (j + 1 == n || chars[j + 1] == ' ') { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j); + // 把 i 置为下一个单词的首字母 + i = j + 2; + break; + } + } + } + // 最后得到题目想要的结果 + return new String(chars); + } + + // 翻转 arr[i..j] + void reverse(char[] arr, int i, int j) { + while (i < j) { + char temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @return {string} + */ +var reverseWords = function(s) { + let sb = ""; + // 先清洗一下数据,把多于的空格都删掉 + for (let i = 0; i < s.length; i++) { + let c = s.charAt(i); + if (c !== ' ') { + // 单词中的字母/数字 + sb += c; + } else if (sb.length > 0 && sb.charAt(sb.length - 1) !== ' ') { + // 单词之间保留一个空格 + sb += ' '; + } + } + // 末尾如果有空格,清除之 + if (sb.charAt(sb.length - 1) === ' ') { + sb = sb.slice(0, sb.length - 1); + } + + // 清洗之后的字符串 + let chars = sb.split(''); + let n = chars.length; + // 进行单词的翻转,先整体翻转 + reverse(chars, 0, n - 1); + // 再把每个单词翻转 + for (let i = 0; i < n; ) { + for (let j = i; j < n; j++) { + if (j + 1 === n || chars[j + 1] === ' ') { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j); + // 把 i 置为下一个单词的首字母 + i = j + 2; + break; + } + } + } + // 最后得到题目想要的结果 + return chars.join(''); +}; + +// 翻转 arr[i..j] +function reverse(arr, i, j) { + while (i < j) { + let temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def reverseWords(self, s: str) -> str: + # 先清洗一下数据,把多于的空格都删掉 + sb = [] + for c in s: + if c != ' ': + # 单词中的字母/数字 + sb.append(c) + elif sb and sb[-1] != ' ': + # 单词之间保留一个空格 + sb.append(' ') + if not sb: + return "" + # 末尾如果有空格,清除之 + if sb[-1] == ' ': + sb.pop() + + # 清洗之后的字符串 + chars = ''.join(sb) + chars = list(chars) + n = len(chars) + # 进行单词的翻转,先整体翻转 + self.reverse(chars, 0, n - 1) + # 再把每个单词翻转 + i = 0 + while i < n: + j = i + while j < n: + if j + 1 == n or chars[j + 1] == ' ': + # chars[i..j] 是一个单词,翻转之 + self.reverse(chars, i, j) + # 把 i 置为下一个单词的首字母 + i = j + 2 + break + j += 1 + # 最后得到题目想要的结果 + return ''.join(chars) + + # 翻转 arr[i..j] + def reverse(self, arr, i, j): + while i < j: + arr[i], arr[j] = arr[j], arr[i] + i += 1 + j -= 1 +``` + +https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* reverseList(ListNode* head) { + // 如果链表为空或只有一个节点,则直接返回该链表 + if (head == nullptr || head->next == nullptr) { + return head; + } + ListNode* last = nullptr; + // 开始迭代翻转节点 + while (head != nullptr) { + // 先保存当前节点的后继节点 + ListNode* next = head->next; + // 将当前节点的指针指向前一个节点 + head->next = last; + // 更新前一个节点为当前节点 + last = head; + // 更新当前节点为后继节点 + head = next; + } + // 翻转链表完成,返回头节点 + return last; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ + +func reverseList(head *ListNode) *ListNode { + // 如果链表为空或者链表只有一个节点,直接返回head + if head == nil || head.Next == nil { + return head + } + // 递归处理下一个节点 + last := reverseList(head.Next) + // 对于当前节点: + // 原来指向下一个节点的指针反转指向上一个节点 + head.Next.Next = head + // 原来指向下一个节点的指针指向nil + head.Next = nil + // 返回链表的最后一个节点,即反转后链表的头节点 + return last +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode last = reverseList(head.next); + /** + ![](../pictures/反转链表/3.jpg) + */ + head.next.next = head; + /** + ![](../pictures/反转链表/4.jpg) + */ + head.next = null; + /** + ![](../pictures/反转链表/5.jpg) + */ + return last; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} head + * @return {ListNode} + */ +var reverseList = function(head) { + if (head === null || head.next === null) { + return head; + } + var last = reverseList(head.next); + /** + ![](../pictures/反转链表/3.jpg) + */ + head.next.next = head; + /** + ![](../pictures/反转链表/4.jpg) + */ + head.next = null; + /** + ![](../pictures/反转链表/5.jpg) + */ + return last; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def reverseList(self, head: ListNode) -> ListNode: + # 如果链表为空或链表只有一个元素,则直接返回head + if head == None or head.next==None: + return head + # 递归反转链表后面的部分 + last = self.reverseList(head.next) + """ + 反转之前的链表 + head --> 3 --> 4 --> 5 --> null + last --> 5 --> null + """ + # 将链表的下一个节点的后继指针指向当前节点 + head.next.next = head + """ + 反转之后的链表 + last --> 5 --> 4 --> 3 --> null + ↑ + head --> 4 ---┘ + """ + # 将当前节点的后继指针指向null + head.next = None + """ + 反转之后的链表 + last --> 5 --> 4 --> 3 --> null + head --> null + """ + # 返回反转后的链表 + return last +``` + +https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof 的多语言解法👆 + +https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int fib(int n) { + if (n == 0 || n == 1) { + // base case + return n; + } + int Q = 1000000007; + // 分别代表 dp[i - 1] 和 dp[i - 2] + int dp_i_1 = 1, dp_i_2 = 0; + for (int i = 2; i <= n; i++) { + // dp[i] = dp[i - 1] + dp[i - 2]; + int dp_i = (dp_i_1 % Q + dp_i_2 % Q) % Q; + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i_1; + } +}; +``` + +```go +// by chatGPT (go) +func fib(n int) int { + if n == 0 || n == 1 { + // base case + return n + } + Q := 1000000007 + // 分别代表 dp[i - 1] 和 dp[i - 2] + dp_i_1, dp_i_2 := 1, 0 + for i := 2; i <= n; i++ { + // dp[i] = dp[i - 1] + dp[i - 2]; + dp_i := (dp_i_1 % Q + dp_i_2 % Q) % Q + dp_i_2 = dp_i_1 + dp_i_1 = dp_i + } + return dp_i_1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int fib(int n) { + if (n == 0 || n == 1) { + // base case + return n; + } + int Q = 1000000007; + // 分别代表 dp[i - 1] 和 dp[i - 2] + int dp_i_1 = 1, dp_i_2 = 0; + for (int i = 2; i <= n; i++) { + // dp[i] = dp[i - 1] + dp[i - 2]; + int dp_i = (dp_i_1 % Q + dp_i_2 % Q) % Q; + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i_1; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number} + */ +var fib = function(n) { + if (n === 0 || n === 1) { + // base case + return n; + } + const Q = 1000000007; + // 分别代表 dp[i - 1] 和 dp[i - 2] + let dp_i_1 = 1, dp_i_2 = 0; + for (let i = 2; i <= n; i++) { + // dp[i] = dp[i - 1] + dp[i - 2]; + const dp_i = (dp_i_1 % Q + dp_i_2 % Q) % Q; + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i_1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def fib(self, n: int) -> int: + if n == 0 or n == 1: + # base case + return n + Q = 1000000007 + # 分别代表 dp[i - 1] 和 dp[i - 2] + dp_i_1, dp_i_2 = 1, 0 + for i in range(2, n + 1): + # dp[i] = dp[i - 1] + dp[i - 2]; + dp_i = (dp_i_1 % Q + dp_i_2 % Q) % Q + dp_i_2 = dp_i_1 + dp_i_1 = dp_i + return dp_i_1 +``` + +https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof 的多语言解法👆 + +https://leetcode.cn/problems/fibonacci-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int fib(int n) { + if (n == 0 || n == 1) { + // base case + return n; + } + // 分别代表 dp[i - 1] 和 dp[i - 2] + int dp_i_1 = 1, dp_i_2 = 0; + for (int i = 2; i <= n; i++) { + // dp[i] = dp[i - 1] + dp[i - 2]; + int dp_i = dp_i_1 + dp_i_2; + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i_1; + } +}; +``` + +```go +// by chatGPT (go) +func fib(n int) int { + if n == 0 || n == 1 { + // base case + return n + } + // 分别代表 dp[i - 1] 和 dp[i - 2] + dp_i_1, dp_i_2 := 1, 0 + for i := 2; i <= n; i++ { + // dp[i] = dp[i - 1] + dp[i - 2]; + dp_i := dp_i_1 + dp_i_2 + dp_i_2 = dp_i_1 + dp_i_1 = dp_i + } + return dp_i_1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int fib(int n) { + if (n == 0 || n == 1) { + // base case + return n; + } + // 分别代表 dp[i - 1] 和 dp[i - 2] + int dp_i_1 = 1, dp_i_2 = 0; + for (int i = 2; i <= n; i++) { + // dp[i] = dp[i - 1] + dp[i - 2]; + int dp_i = dp_i_1 + dp_i_2; + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i_1; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number} + */ +var fib = function(n) { + if (n === 0 || n === 1) { + // base case + return n; + } + // 分别代表 dp[i - 1] 和 dp[i - 2] + let dp_i_1 = 1, dp_i_2 = 0; + for (let i = 2; i <= n; i++) { + // dp[i] = dp[i - 1] + dp[i - 2]; + const dp_i = dp_i_1 + dp_i_2; + dp_i_2 = dp_i_1; + dp_i_1 = dp_i; + } + return dp_i_1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def fib(self, n: int) -> int: + if n == 0 or n == 1: + # base case + return n + # 分别代表 dp[i - 1] 和 dp[i - 2] + dp_i_1, dp_i_2 = 1, 0 + for i in range(2, n+1): + # dp[i] = dp[i - 1] + dp[i - 2]; + dp_i = dp_i_1 + dp_i_2 + dp_i_2 = dp_i_1 + dp_i_1 = dp_i + return dp_i_1 +``` + +https://leetcode.cn/problems/fibonacci-number 的多语言解法👆 + +https://leetcode.cn/problems/find-all-anagrams-in-a-string 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector findAnagrams(string s, string t) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + vector res; // 记录结果 + while (right < s.size()) { + char c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + if (need.count(c)) { + window[c]++; + if (window[c] == need[c]) + valid++; + } + // 判断左侧窗口是否要收缩 + while (right - left >= t.size()) { + // 当窗口符合条件时,把起始索引加入 res + if (valid == need.size()) + res.push_back(left); + char d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + if (need.count(d)) { + if (window[d] == need[d]) + valid--; + window[d]--; + } + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func findAnagrams(s string, t string) []int { + need := make(map[byte]int) + window := make(map[byte]int) + for i := 0; i < len(t); i++ { + need[t[i]]++ + } + + left, right := 0, 0 + valid := 0 + var res []int + for right < len(s) { + c := s[right] + right++ + // 进行窗口内数据的一系列更新 + if _, ok := need[c]; ok { + window[c]++ + if window[c] == need[c] { + valid++ + } + } + // 判断左侧窗口是否要收缩 + for right - left >= len(t) { + // 当窗口符合条件时,把起始索引加入 res + if valid == len(need) { + res = append(res, left) + } + d := s[left] + left++ + // 进行窗口内数据的一系列更新 + if _, ok := need[d]; ok { + if window[d] == need[d] { + valid-- + } + window[d]-- + } + } + } + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + public List findAnagrams(String s, String t) { + Map need = new HashMap<>(); + Map window = new HashMap<>(); + for (char c : t.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1); + + int left = 0, right = 0; + int valid = 0; + List res = new ArrayList<>(); + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) + valid++; + } + // 判断左侧窗口是否要收缩 + while (right - left >= t.length()) { + // 当窗口符合条件时,把起始索引加入 res + if (valid == need.size()) + res.add(left); + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) + valid--; + window.put(d, window.get(d) - 1); + } + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @param {string} t + * @return {number[]} + */ +var findAnagrams = function(s, t) { + const need = new Map(); + const window = new Map(); + for (const c of t) { + need.set(c, (need.get(c) || 0) + 1); + } + + let left = 0, right = 0; + let valid = 0; + const res = []; + + while (right < s.length) { + const c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + if (need.has(c)) { + window.set(c, (window.get(c) || 0) + 1); + if (window.get(c) === need.get(c)) { + valid++; + } + } + // 判断左侧窗口是否要收缩 + while (right - left >= t.length) { + // 当窗口符合条件时,把起始索引加入 res + if (valid === need.size) { + res.push(left); + } + const d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + if (need.has(d)) { + if (window.get(d) === need.get(d)) { + valid--; + } + window.set(d, window.get(d) - 1); + } + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findAnagrams(self, s: str, t: str) -> List[int]: + need = {} + window = {} + for c in t: + if c in need: + need[c] += 1 + else: + need[c] = 1 + + left = 0 + right = 0 + valid = 0 + res = [] + + while right < len(s): + c = s[right] + right += 1 + # 进行窗口内数据的一系列更新 + if c in need: + if c in window: + window[c] += 1 + else: + window[c] = 1 + if window[c] == need[c]: + valid += 1 + # 判断左侧窗口是否要收缩 + while right - left >= len(t): + # 当窗口符合条件时,把起始索引加入 res + if valid == len(need): + res.append(left) + d = s[left] + left += 1 + # 进行窗口内数据的一系列更新 + if d in need: + if window[d] == need[d]: + valid -= 1 + window[d] -= 1 + + return res +``` + +https://leetcode.cn/problems/find-all-anagrams-in-a-string 的多语言解法👆 + +https://leetcode.cn/problems/find-all-duplicates-in-an-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector findDuplicates(vector& nums) { + int n = nums.size(); + vector res; + // 用数组模拟哈希集合 + vector seen(n + 1); + for (int num : nums) { + if (seen[num] == 0) { + // 添加到哈希集合 + seen[num] = 1; + } else { + // 找到重复元素 + res.push_back(num); + } + } + return res; + } +}; + +class Solution2 { +public: + vector findDuplicates(vector& nums) { + vector res; + for (int num : nums) { + // 注意索引,元素大小从 1 开始,有一位索引偏移 + if (nums[abs(num) - 1] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + res.push_back(abs(num)); + } else { + // 把索引 num - 1 置为负数 + nums[abs(num) - 1] *= -1; + } + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func findDuplicates(nums []int) []int { + n := len(nums) + res := make([]int, 0) + // 用数组模拟哈希集合 + seen := make([]int, n+1) + for _, num := range nums { + if seen[num] == 0 { + // 添加到哈希集合 + seen[num] = 1 + } else { + // 找到重复元素 + res = append(res, num) + } + } + return res +} + +func findDuplicates2(nums []int) []int { + res := make([]int, 0) + for _, num := range nums { + // 注意索引,元素大小从 1 开始,有一位索引偏移 + if nums[abs(num)-1] < 0 { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + res = append(res, abs(num)) + } else { + // 把索引 num - 1 置为负数 + nums[abs(num)-1] *= -1 + } + } + + return res +} + +func abs(a int) int { + if a < 0 { + return -a + } + return a +} +``` + +```java +// by labuladong (java) +class Solution { + public List findDuplicates(int[] nums) { + int n = nums.length; + List res = new LinkedList<>(); + // 用数组模拟哈希集合 + int[] seen = new int[n + 1]; + for (int num : nums) { + if (seen[num] == 0) { + // 添加到哈希集合 + seen[num] = 1; + } else { + // 找到重复元素 + res.add(num); + } + } + return res; + } +} + +class Solution2 { + public List findDuplicates(int[] nums) { + List res = new LinkedList<>(); + for (int num : nums) { + // 注意索引,nums 中元素大小从 1 开始, + // 而索引是从 0 开始的,所以有一位索引偏移 + if (nums[Math.abs(num) - 1] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + res.add(Math.abs(num)); + } else { + // 把索引 num - 1 置为负数 + nums[Math.abs(num) - 1] *= -1; + } + } + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findDuplicates = function(nums) { + const n = nums.length; + const res = []; + // 用数组模拟哈希集合 + const seen = new Array(n + 1).fill(0); + for (let i = 0; i < n; i++) { + const num = nums[i]; + if (seen[num] === 0) { + // 添加到哈希集合 + seen[num] = 1; + } else { + // 找到重复元素 + res.push(num); + } + } + return res; +}; + +var findDuplicates2 = function(nums) { + const res = []; + for (let i = 0; i < nums.length; i++) { + const num = Math.abs(nums[i]); + // 注意索引,元素大小从 1 开始,有一位索引偏移 + if (nums[num - 1] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + res.push(num); + } else { + // 把索引 num - 1 置为负数 + nums[num - 1] *= -1; + } + } + + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findDuplicates(self, nums: List[int]) -> List[int]: + n = len(nums) + res = [] + # 用数组模拟哈希集合 + seen = [0] * (n + 1) + for num in nums: + if seen[num] == 0: + # 添加到哈希集合 + seen[num] = 1 + else: + # 找到重复元素 + res.append(num) + return res + + +class Solution2: + def findDuplicates(self, nums: List[int]) -> List[int]: + res = [] + for num in nums: + # 注意索引,元素大小从 1 开始,有一位索引偏移 + if nums[abs(num) - 1] < 0: + # 之前已经把对应索引的元素变成负数了, + # 这说明 num 重复出现了两次 + res.append(abs(num)) + else: + # 把索引 num - 1 置为负数 + nums[abs(num) - 1] *= -1 + return res +``` + +https://leetcode.cn/problems/find-all-duplicates-in-an-array 的多语言解法👆 + +https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector findDisappearedNumbers(vector& nums) { + int n = nums.size(); + vector count(n + 1, 0); + for (int num : nums) { + count[num]++; + } + vector res; + for (int num = 1; num <= n; num++) { + if (count[num] == 0) { + res.push_back(num); + } + } + return res; + } +}; + +class Solution2 { +public: + vector findDisappearedNumbers(vector& nums) { + for (int num : nums) { + // 注意索引,元素大小从 1 开始,有一位索引偏移 + if (nums[abs(num) - 1] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次,但我们不用做,让索引继续保持负数 + } else { + // 把索引 num - 1 置为负数 + nums[abs(num) - 1] *= -1; + } + } + + vector res; + for (int i = 0; i < nums.size(); i++) { + if (nums[i] > 0) { + // 说明没有元素和这个索引对应,即找到一个缺失元素 + res.push_back(i + 1); + } + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func findDisappearedNumbers(nums []int) []int { + n := len(nums) + count := make([]int, n+1) + for _, num := range nums { + count[num]++ + } + res := make([]int, 0) + for num := 1; num <= n; num++ { + if count[num] == 0 { + res = append(res, num) + } + } + return res +} + + +func findDisappearedNumbers2(nums []int) []int { + for _, num := range nums { + // 注意索引,元素大小从 1 开始,有一位索引偏移 + if nums[abs(num)-1] < 0 { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次,但我们不用做,让索引继续保持负数 + } else { + // 把索引 num - 1 置为负数 + nums[abs(num)-1] *= -1 + } + } + + res := make([]int, 0) + for i, num := range nums { + if num > 0 { + // 说明没有元素和这个索引对应,即找到一个缺失元素 + res = append(res, i+1) + } + } + + return res +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} +``` + +```java +// by labuladong (java) +class Solution { + public List findDisappearedNumbers(int[] nums) { + int n = nums.length; + int[] count = new int[n + 1]; + for (int num : nums) { + count[num]++; + } + List res = new LinkedList<>(); + for (int num = 1; num <= n; num++) { + if (count[num] == 0) { + res.add(num); + } + } + return res; + } +} + + +class Solution2 { + public List findDisappearedNumbers(int[] nums) { + for (int num : nums) { + // 注意索引,nums 中元素大小从 1 开始, + // 而索引是从 0 开始的,所以有一位索引偏移 + if (nums[Math.abs(num) - 1] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次,但我们什么都不用做,让索引继续保持负数 + } else { + // 把索引 num - 1 置为负数 + nums[Math.abs(num) - 1] *= -1; + } + } + + List res = new LinkedList<>(); + for (int i = 0; i < nums.length; i++) { + if (nums[i] > 0) { + // 说明没有元素和这个索引对应,即找到一个缺失元素 + res.add(i + 1); + } + } + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findDisappearedNumbers = function(nums) { + const n = nums.length; + const count = new Array(n+1).fill(0); + for (let i = 0; i < n; i++) { + count[nums[i]]++; + } + const res = []; + for (let i = 1; i <= n; i++) { + if (count[i] === 0) { + res.push(i); + } + } + return res; +} + +var findDisappearedNumbers2 = function(nums) { + for (let i = 0; i < nums.length; i++) { + const num = Math.abs(nums[i]); + // 注意索引,元素大小从 1 开始,有一位索引偏移 + if (nums[num - 1] > 0) { + // 把索引 num - 1 置为负数 + nums[num - 1] *= -1; + } + } + + const res = []; + for (let i = 0; i < nums.length; i++) { + if (nums[i] > 0) { + // 说明没有元素和这个索引对应,即找到一个缺失元素 + res.push(i + 1); + } + } + + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def findDisappearedNumbers(self, nums: List[int]) -> List[int]: + n = len(nums) + count = [0] * (n + 1) + for num in nums: + count[num] += 1 + res = [] + for num in range(1, n+1): + if count[num] == 0: + res.append(num) + return res + +class Solution2: + def findDisappearedNumbers(self, nums: List[int]) -> List[int]: + for num in nums: + # 注意索引,元素大小从 1 开始,有一位索引偏移 + if nums[abs(num) - 1] < 0: + # 之前已经把对应索引的元素变成负数了, + # 这说明 num 重复出现了两次,但我们不用做,让索引继续保持负数 + pass + else: + # 把索引 num - 1 置为负数 + nums[abs(num) - 1] *= -1 + + res = [] + for i in range(len(nums)): + if nums[i] > 0: + # 说明没有元素和这个索引对应,即找到一个缺失元素 + res.append(i + 1) + + return res +``` + +https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array 的多语言解法👆 + +https://leetcode.cn/problems/find-distance-in-a-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findDistance(TreeNode* root, int p, int q) { + lca(root, p, q); + return res; + } + +private: + bool found = false; + int res = 0; + + // 定义:当子树中不包含 p 或 q 时,返回 0; + // 当子树中仅包含 p 或 q 中的一个时,返回 root 到 p 或 q 的距离; + // 当子树中同时包含 p 和 q 时,返回一个无意义的值(答案会被存在外部变量 res 中) + int lca(TreeNode* root, int p, int q) { + if (found) { + // found 为 true 时答案已经被记录在全局 res 中 + // 递归函数的返回值已不需要了,此时返回什么值都无所谓 + return 123; + } + if (!root) { + return 0; + } + + int left = lca(root->left, p, q); + int right = lca(root->right, p, q); + + if (left == 0 && right == 0) { + // root 的左右子树都不包含 p 或 q + if (root->val == p || root->val == q) { + return 1; + } + return 0; + } + + if (left != 0 && right != 0 && !found) { + // 当前节点 root 就是 p, q 的最近公共祖先节点 + // 答案已经算出来了,更新全局变量 + res = left + right; + // 这个递归函数的返回值已经不重要了,让它终止递归 + found = true; + return 456; + } + + // 此时 left 和 right 有一个为 0,即只找到了一个节点 + // branch 就是到该节点的距离 + int branch = left == 0 ? right : left; + + if (root->val == p || root->val == q) { + res = branch; + } + + return branch + 1; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func findDistance(root *TreeNode, p int, q int) int { + var found bool = false + var res int = 0 + + // 定义:当子树中不包含 p 或 q 时,返回 0; + // 当子树中仅包含 p 或 q 中的一个时,返回 root 到 p 或 q 的距离; + // 当子树中同时包含 p 和 q 时,返回一个无意义的值(答案会被存在外部变量 res 中) + var lca func(root *TreeNode, p, q int) int + lca = func(root *TreeNode, p, q int) int { + if found { + // found 为 true 时答案已经被记录在全局 res 中 + // 递归函数的返回值已不需要了,此时返回什么值都无所谓 + return 123 + } + if root == nil { + return 0 + } + + left := lca(root.Left, p, q) + right := lca(root.Right, p, q) + + if left == 0 && right == 0 { + // root 的左右子树都不包含 p 或 q + if root.Val == p || root.Val == q { + return 1 + } + return 0 + } + + if left != 0 && right != 0 && !found { + // 当前节点 root 就是 p, q 的最近公共祖先节点 + // 答案已经算出来了,更新全局变量 + res = left + right + // 这个递归函数的返回值已经不重要了,让它终止递归 + found = true + return 456 + } + + // 此时 left 和 right 有一个为 0,即只找到了一个节点 + // branch 就是到该节点的距离 + branch := 0 + if left == 0 { + branch = right + } else { + branch = left + } + + if root.Val == p || root.Val == q { + res = branch + } + + return branch + 1 + } + lca(root, p, q) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int findDistance(TreeNode root, int p, int q) { + lca(root, p, q); + return res; + } + + + boolean found = false; + int res = 0; + + // 定义:当子树中不包含 p 或 q 时,返回 0; + // 当子树中仅包含 p 或 q 中的一个时,返回 root 到 p 或 q 的距离; + // 当子树中同时包含 p 和 q 时,返回一个无意义的值(答案会被存在外部变量 res 中) + int lca(TreeNode root, int p, int q) { + if (found) { + // found 为 true 时答案已经被记录在全局 res 中 + // 递归函数的返回值已不需要了,此时返回什么值都无所谓 + return 123; + } + if (root == null) { + return 0; + } + + int left = lca(root.left, p, q); + int right = lca(root.right, p, q); + + if (left == 0 && right == 0) { + // root 的左右子树都不包含 p 或 q + if (root.val == p || root.val == q) { + return 1; + } + return 0; + } + + if (left != 0 && right != 0 && !found) { + // 当前节点 root 就是 p, q 的最近公共祖先节点 + // 答案已经算出来了,更新全局变量 + res = left + right; + // 这个递归函数的返回值已经不重要了,让它终止递归 + found = true; + return 456; + } + + // 此时 left 和 right 有一个为 0,即只找到了一个节点 + // branch 就是到该节点的距离 + int branch = left == 0 ? right : left; + + if (root.val == p || root.val == q) { + res = branch; + } + + return branch + 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findDistance = function(root, p, q) { + var found = false; + var res = 0; + + // 定义:当子树中不包含 p 或 q 时,返回 0; + // 当子树中仅包含 p 或 q 中的一个时,返回 root 到 p 或 q 的距离; + // 当子树中同时包含 p 和 q 时,返回一个无意义的值(答案会被存在外部变量 res 中) + var lca = function(root, p, q) { + if (found) { + // found 为 true 时答案已经被记录在全局 res 中 + // 递归函数的返回值已不需要了,此时返回什么值都无所谓 + return 123; + } + if (root == null) { + return 0; + } + + var left = lca(root.left, p, q); + var right = lca(root.right, p, q); + + if (left == 0 && right == 0) { + // root 的左右子树都不包含 p 或 q + if (root.val == p || root.val == q) { + return 1; + } + return 0; + } + + if (left != 0 && right != 0 && !found) { + // 当前节点 root 就是 p, q 的最近公共祖先节点 + // 答案已经算出来了,更新全局变量 + res = left + right; + // 这个递归函数的返回值已经不重要了,让它终止递归 + found = true; + return 456; + } + + // 此时 left 和 right 有一个为 0,即只找到了一个节点 + // branch 就是到该节点的距离 + var branch = left == 0 ? right : left; + + if (root.val == p || root.val == q) { + res = branch; + } + + return branch + 1; + }; + + lca(root, p, q); + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def findDistance(self, root: TreeNode, p: int, q: int) -> int: + self.found = False + self.res = 0 + self.lca(root, p, q) + return self.res + + # 定义:当子树中不包含 p 或 q 时,返回 0; + # 当子树中仅包含 p 或 q 中的一个时,返回 root 到 p 或 q 的距离; + # 当子树中同时包含 p 和 q 时,返回一个无意义的值(答案会被存在外部变量 res 中) + def lca(self, root: TreeNode, p: int, q: int) -> int: + if self.found: + # found 为 true 时答案已经被记录在全局 res 中 + # 递归函数的返回值已不需要了,此时返回什么值都无所谓 + return 123 + if root is None: + return 0 + + left = self.lca(root.left, p, q) + right = self.lca(root.right, p, q) + + if left == 0 and right == 0: + # root 的左右子树都不包含 p 或 q + if root.val == p or root.val == q: + return 1 + return 0 + + if left != 0 and right != 0 and not self.found: + # 当前节点 root 就是 p, q 的最近公共祖先节点 + # 答案已经算出来了,更新全局变量 + self.res = left + right + # 这个递归函数的返回值已经不重要了,让它终止递归 + self.found = True + return 456 + + # 此时 left 和 right 有一个为 0,即只找到了一个节点 + # branch 就是到该节点的距离 + branch = right if left == 0 else left + + if root.val == p or root.val == q: + self.res = branch + + return branch + 1 +``` + +https://leetcode.cn/problems/find-distance-in-a-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/find-duplicate-subtrees 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +#include +#include +using namespace std; + +class Solution { + // 记录所有子树以及出现的次数 + unordered_map memo; + // 记录重复的子树根节点 + vector res; + +public: + /* 主函数 */ + vector findDuplicateSubtrees(TreeNode* root) { + traverse(root); + return res; + } + + string traverse(TreeNode* root) { + if (root == nullptr) { + return "#"; + } + + string left = traverse(root->left); + string right = traverse(root->right); + + string subTree = left + "," + right + "," + to_string(root->val); + + int freq = memo[subTree]; + // 多次重复也只会被加入结果集一次 + if (freq == 1) { + res.push_back(root); + } + // 给子树对应的出现次数加一 + memo[subTree] = freq + 1; + return subTree; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "strconv" +) + +// TreeNode Definition for a binary tree node. + +// findDuplicateSubtrees 记录所有子树以及出现的次数 +func findDuplicateSubtrees(root *TreeNode) []*TreeNode { + memo := make(map[string]int) + res := []*TreeNode{} + + traverse(root, memo, &res) + + return res +} + +// traverse 深度优先遍历 +func traverse(root *TreeNode, memo map[string]int, res *[]*TreeNode) string { + if root == nil { + return "#" + } + + left := traverse(root.Left, memo, res) + right := traverse(root.Right, memo, res) + + subTree := left + "," + right + "," + strconv.Itoa(root.Val) + + freq := memo[subTree] + // 多次重复也只会被加入结果集一次 + if freq == 1 { + *res = append(*res, root) + } + // 给子树对应的出现次数加一 + memo[subTree]++ + return subTree +} +``` + +```java +// by labuladong (java) +class Solution { + // 记录所有子树以及出现的次数 + HashMap memo = new HashMap<>(); + // 记录重复的子树根节点 + LinkedList res = new LinkedList<>(); + + /* 主函数 */ + public List findDuplicateSubtrees(TreeNode root) { + traverse(root); + return res; + } + + String traverse(TreeNode root) { + if (root == null) { + return "#"; + } + + String left = traverse(root.left); + String right = traverse(root.right); + + String subTree = left + "," + right + "," + root.val; + + int freq = memo.getOrDefault(subTree, 0); + // 多次重复也只会被加入结果集一次 + if (freq == 1) { + res.add(root); + } + // 给子树对应的出现次数加一 + memo.put(subTree, freq + 1); + return subTree; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findDuplicateSubtrees = function(root) { + // 记录所有子树以及出现的次数 + const memo = new Map(); + // 记录重复的子树根节点 + const res = []; + + /* 主函数 */ + const traverse = function(root) { + if (root == null) { + return "#"; + } + + const left = traverse(root.left); + const right = traverse(root.right); + + const subTree = left + "," + right + "," + root.val; + + const freq = memo.get(subTree) || 0; + // 多次重复也只会被加入结果集一次 + if (freq === 1) { + res.push(root); + } + // 给子树对应的出现次数加一 + memo.set(subTree, freq + 1); + return subTree; + }; + + traverse(root); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findDuplicateSubtrees(self, root: TreeNode) -> List[TreeNode]: + memo = {} + res = [] + + def traverse(root): + if not root: + return "#" + + left = traverse(root.left) + right = traverse(root.right) + + subTree = left + "," + right + "," + str(root.val) + + freq = memo.get(subTree, 0) + # 多次重复也只会被加入结果集一次 + if freq == 1: + res.append(root) + # 给子树对应的出现次数加一 + memo[subTree] = freq + 1 + return subTree + + traverse(root) + return res +``` + +https://leetcode.cn/problems/find-duplicate-subtrees 的多语言解法👆 + +https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector searchRange(vector& nums, int target) { + int left = left_bound(nums, target); + int right = right_bound(nums, target); + return {left, right}; + } + + int left_bound(vector& nums, int target) { + int left = 0, right = nums.size() - 1; + // 搜索区间为 [left, right] + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1; + } else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1; + } else if (nums[mid] == target) { + // 收缩右侧边界 + right = mid - 1; + } + } + // 检查出界情况 + if (left >= nums.size() || nums[left] != target) { + /* + ![](../pictures/二分查找/2.jpg) + */ + return -1; + } + return left; + } + + int right_bound(vector& nums, int target) { + int left = 0, right = nums.size() - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // 这里改成收缩左侧边界即可 + left = mid + 1; + } + } + // 这里改为检查 right 越界的情况,见下图 + if (right < 0 || nums[right] != target) { + /* + ![](../pictures/二分查找/4.jpg) + */ + return -1; + } + return right; + } +}; +``` + +```go +// by chatGPT (go) +func searchRange(nums []int, target int) []int { + return []int{leftBound(nums, target), rightBound(nums, target)} +} + +func leftBound(nums []int, target int) int { + left, right := 0, len(nums)-1 + // 搜索区间为 [left, right] + for left <= right { + mid := left + (right-left)/2 + if nums[mid] < target { + // 搜索区间变为 [mid+1, right] + left = mid + 1 + } else if nums[mid] > target { + // 搜索区间变为 [left, mid-1] + right = mid - 1 + } else if nums[mid] == target { + // 收缩右侧边界 + right = mid - 1 + } + } + // 检查出界情况 + if left >= len(nums) || nums[left] != target { + /** + ![](../pictures/二分查找/2.jpg) + */ + return -1 + } + return left +} + +func rightBound(nums []int, target int) int { + left, right := 0, len(nums)-1 + for left <= right { + mid := left + (right-left)/2 + if nums[mid] < target { + left = mid + 1 + } else if nums[mid] > target { + right = mid - 1 + } else if nums[mid] == target { + // 这里改成收缩左侧边界即可 + left = mid + 1 + } + } + // 这里改为检查 right 越界的情况,见下图 + if right < 0 || nums[right] != target { + /** + ![](../pictures/二分查找/4.jpg) + */ + return -1 + } + return right +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] searchRange(int[] nums, int target) { + return new int[]{left_bound(nums, target), right_bound(nums, target)}; + } + + int left_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + // 搜索区间为 [left, right] + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1; + } else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1; + } else if (nums[mid] == target) { + // 收缩右侧边界 + right = mid - 1; + } + } + // 检查出界情况 + if (left >= nums.length || nums[left] != target) { + /** + ![](../pictures/二分查找/2.jpg) + */ + return -1; + } + return left; + } + + int right_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // 这里改成收缩左侧边界即可 + left = mid + 1; + } + } + // 这里改为检查 right 越界的情况,见下图 + if (right < 0 || nums[right] != target) { + /** + ![](../pictures/二分查找/4.jpg) + */ + return -1; + } + return right; + } +} +``` + +```javascript +// by chatGPT (javascript) +var searchRange = function(nums, target) { + return [left_bound(nums, target), right_bound(nums, target)]; +}; + +var left_bound = function(nums, target) { + let left = 0, + right = nums.length - 1; + // 搜索区间为 [left, right] + while (left <= right) { + let mid = left + Math.floor((right - left) / 2); + if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1; + } else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1; + } else if (nums[mid] === target) { + // 收缩右侧边界 + right = mid - 1; + } + } + // 检查出界情况 + if (left >= nums.length || nums[left] !== target) { + /** + ![](../pictures/二分查找/2.jpg) + */ + return -1; + } + return left; +}; + +var right_bound = function(nums, target) { + let left = 0,right = nums.length - 1; + while (left <= right) { + let mid = left + Math.floor((right - left) / 2); + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] === target) { + // 这里改成收缩左侧边界即可 + left = mid + 1; + } + } + // 这里改为检查 right 越界的情况,见下图 + if (right < 0 || nums[right] !== target) { + /** + ![](../pictures/二分查找/4.jpg) + */ + return -1; + } + return right; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def searchRange(self, nums: List[int], target: int) -> List[int]: + return [self.left_bound(nums, target), self.right_bound(nums, target)] + + def left_bound(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) - 1 + # 搜索区间为 [left, right] + while left <= right: + mid = left + (right - left) // 2 + if nums[mid] < target: + # 搜索区间变为 [mid+1, right] + left = mid + 1 + elif nums[mid] > target: + # 搜索区间变为 [left, mid-1] + right = mid - 1 + elif nums[mid] == target: + # 收缩右侧边界 + right = mid - 1 + # 检查出界情况 + if left >= len(nums) or nums[left] != target: + """ + + ![](../pictures/二分查找/2.jpg) + """f + return -1 + return left + + def right_bound(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) - 1 + while left <= right: + mid = left + (right - left) // 2 + if nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid - 1 + elif nums[mid] == target: + # 这里改成收缩左侧边界即可 + left = mid + 1 + # 这里改为检查 right 越界的情况,见下图 + if right < 0 or nums[right] != target: + ''' + + ![](../pictures/二分查找/4.jpg) + ''' + return -1 + return right +``` + +https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array 的多语言解法👆 + +https://leetcode.cn/problems/find-k-closest-elements 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector findClosestElements(vector& arr, int k, int x) { + // 二分搜索找到 x 的位置 + int p = left_bound(arr, x); + // 两端都开的区间 (left, right) + int left = p - 1, right = p; + vector res; + // 扩展区间,直到区间内包含 k 个元素 + while (right - left - 1 < k) { + if (left == -1) { + res.push_back(arr[right]); + right++; + } else if (right == arr.size()) { + res.insert(res.begin(), arr[left]); + left--; + } else if (x - arr[left] > arr[right] - x) { + res.push_back(arr[right]); + right++; + } else { + res.insert(res.begin(), arr[left]); + left--; + } + } + return res; + } + + // 搜索左侧边界的二分搜索 + int left_bound(vector& nums, int target) { + int left = 0; + int right = nums.size(); + + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left; + } +}; +``` + +```go +// by chatGPT (go) +func findClosestElements(arr []int, k int, x int) []int { + // 二分搜索找到 x 的位置 + p := left_bound(arr, x) + // 两端都开的区间 (left, right) + left, right := p-1, p + res := make([]int, 0) + // 扩展区间,直到区间内包含 k 个元素 + for right-left-1 < k { + if left == -1 { + res = append(res, arr[right]) + right++ + } else if right == len(arr) { + res = append([]int{arr[left]}, res...) + left-- + } else if x-arr[left] > arr[right]-x { + res = append(res, arr[right]) + right++ + } else { + res = append([]int{arr[left]}, res...) + left-- + } + } + return res +} + +// 搜索左侧边界的二分搜索 +func left_bound(nums []int, target int) int { + left := 0 + right := len(nums) + + for left < right { + mid := left + (right-left)/2 + if nums[mid] == target { + right = mid + } else if nums[mid] < target { + left = mid + 1 + } else if nums[mid] > target { + right = mid + } + } + return left +} +``` + +```java +// by labuladong (java) +class Solution { + public List findClosestElements(int[] arr, int k, int x) { + // 二分搜索找到 x 的位置 + int p = left_bound(arr, x); + // 两端都开的区间 (left, right) + int left = p - 1, right = p; + LinkedList res = new LinkedList<>(); + // 扩展区间,直到区间内包含 k 个元素 + while (right - left - 1 < k) { + if (left == -1) { + res.addLast(arr[right]); + right++; + } else if (right == arr.length) { + res.addFirst(arr[left]); + left--; + } else if (x - arr[left] > arr[right] - x) { + res.addLast(arr[right]); + right++; + } else { + res.addFirst(arr[left]); + left--; + } + } + return res; + } + + // 搜索左侧边界的二分搜索 + int left_bound(int[] nums, int target) { + int left = 0; + int right = nums.length; + + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findClosestElements = function(arr, k, x) { + // 二分搜索找到 x 的位置 + let p = left_bound(arr, x); + // 两端都开的区间 (left, right) + let left = p - 1, right = p; + let res = []; + // 扩展区间,直到区间内包含 k 个元素 + while (right - left - 1 < k) { + if (left == -1) { + res.push(arr[right]); + right++; + } else if (right == arr.length) { + res.unshift(arr[left]); + left--; + } else if (x - arr[left] > arr[right] - x) { + res.push(arr[right]); + right++; + } else { + res.unshift(arr[left]); + left--; + } + } + return res; +}; + +// 搜索左侧边界的二分搜索 +var left_bound = function(nums, target) { + let left = 0; + let right = nums.length; + + while (left < right) { + let mid = left + Math.floor((right - left) / 2); + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]: + # 二分搜索找到 x 的位置 + p = self.left_bound(arr, x) + # 两端都开的区间 (left, right) + left, right = p - 1, p + res = [] + # 扩展区间,直到区间内包含 k 个元素 + while right - left - 1 < k: + if left == -1: + res.append(arr[right]) + right += 1 + elif right == len(arr): + res.insert(0, arr[left]) + left -= 1 + elif x - arr[left] > arr[right] - x: + res.append(arr[right]) + right += 1 + else: + res.insert(0, arr[left]) + left -= 1 + return res + + # 搜索左侧边界的二分搜索 + def left_bound(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) + + while left < right: + mid = left + (right - left) // 2 + if nums[mid] == target: + right = mid + elif nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid + return left +``` + +https://leetcode.cn/problems/find-k-closest-elements 的多语言解法👆 + +https://leetcode.cn/problems/find-k-pairs-with-smallest-sums 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> kSmallestPairs(vector& nums1, vector& nums2, int k) { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + priority_queue, vector>, greater>> pq([](const vector& a, const vector& b) { + // 按照数对的元素和升序排序 + return a[0] + a[1] < b[0] + b[1]; + }); + // 按照 23 题的逻辑初始化优先级队列 + for (int i = 0; i < nums1.size(); i++) { + pq.push({nums1[i], nums2[0], 0}); + } + + vector> res; + // 执行合并多个有序链表的逻辑 + while (!pq.empty() && k > 0) { + auto cur = pq.top(); + pq.pop(); + k--; + // 链表中的下一个节点加入优先级队列 + int next_index = cur[2] + 1; + if (next_index < nums2.size()) { + pq.push({cur[0], nums2[next_index], next_index}); + } + + vector pair = {cur[0], cur[1]}; + res.push_back(pair); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func kSmallestPairs(nums1 []int, nums2 []int, k int) [][]int { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + pq := make(PriorityQueue, 0) + heap.Init(&pq) + // 按照 23 题的逻辑初始化优先级队列 + for i := 0; i < len(nums1); i++ { + pq.Push([]int{nums1[i], nums2[0], 0}) + } + + res := make([][]int, 0) + // 执行合并多个有序链表的逻辑 + for pq.Len() > 0 && k > 0 { + cur := pq.Pop().([]int) + k-- + // 链表中的下一个节点加入优先级队列 + next_index := cur[2] + 1 + if next_index < len(nums2) { + pq.Push([]int{cur[0], nums2[next_index], next_index}) + } + + pair := []int{cur[0], cur[1]} + res = append(res, pair) + } + return res +} + +type PriorityQueue [][]int + +func (pq PriorityQueue) Len() int { return len(pq) } + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i][0]+pq[i][1] < pq[j][0]+pq[j][1] +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + *pq = append(*pq, x.([]int)) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} +``` + +```java +// by labuladong (java) +class Solution { + public List> kSmallestPairs(int[] nums1, int[] nums2, int k) { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + PriorityQueue pq = new PriorityQueue<>((a, b) -> { + // 按照数对的元素和升序排序 + return (a[0] + a[1]) - (b[0] + b[1]); + }); + // 按照 23 题的逻辑初始化优先级队列 + for (int i = 0; i < nums1.length; i++) { + pq.offer(new int[]{nums1[i], nums2[0], 0}); + } + + List> res = new ArrayList<>(); + // 执行合并多个有序链表的逻辑 + while (!pq.isEmpty() && k > 0) { + int[] cur = pq.poll(); + k--; + // 链表中的下一个节点加入优先级队列 + int next_index = cur[2] + 1; + if (next_index < nums2.length) { + pq.add(new int[]{cur[0], nums2[next_index], next_index}); + } + + List pair = new ArrayList<>(); + pair.add(cur[0]); + pair.add(cur[1]); + res.add(pair); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var kSmallestPairs = function(nums1, nums2, k) { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + const pq = new PriorityQueue((a, b) => { + // 按照数对的元素和升序排序 + return (a[0] + a[1]) - (b[0] + b[1]); + }); + // 按照 23 题的逻辑初始化优先级队列 + for (let i = 0; i < nums1.length; i++) { + pq.offer([nums1[i], nums2[0], 0]); + } + + const res = []; + // 执行合并多个有序链表的逻辑 + while (!pq.isEmpty() && k > 0) { + const cur = pq.poll(); + k--; + // 链表中的下一个节点加入优先级队列 + const next_index = cur[2] + 1; + if (next_index < nums2.length) { + pq.add([cur[0], nums2[next_index], next_index]); + } + + const pair = [cur[0], cur[1]]; + res.push(pair); + } + return res; +}; + +// 优先级队列的实现,用于存储三元组 +class PriorityQueue { + constructor(comparator) { + this.heap = []; + this.comparator = comparator; + } + + isEmpty() { + return this.heap.length === 0; + } + + size() { + return this.heap.length; + } + + peek() { + return this.heap[0]; + } + + offer(value) { + this.heap.push(value); + this.bubbleUp(this.heap.length - 1); + } + + poll() { + const result = this.heap[0]; + const end = this.heap.pop(); + if (this.heap.length > 0) { + this.heap[0] = end; + this.bubbleDown(0); + } + return result; + } + + bubbleUp(index) { + while (index > 0) { + const parentIndex = (index - 1) >> 1; + if (this.comparator(this.heap[index], this.heap[parentIndex]) < 0) { + const tmp = this.heap[index]; + this.heap[index] = this.heap[parentIndex]; + this.heap[parentIndex] = tmp; + index = parentIndex; + } else { + break; + } + } + } + + bubbleDown(index) { + const lastIndex = this.heap.length - 1; + while (STATUS_PASSED_TEST) { + const leftIndex = index * 2 + 1; + const rightIndex = index * 2 + 2; + let findIndex = index; + if (leftIndex <= lastIndex && this.comparator(this.heap[leftIndex], this.heap[findIndex]) < 0) { + findIndex = leftIndex; + } + if (rightIndex <= lastIndex && this.comparator(this.heap[rightIndex], this.heap[findIndex]) < 0) { + findIndex = rightIndex; + } + if (index !== findIndex) { + const tmp = this.heap[index]; + this.heap[index] = this.heap[findIndex]; + this.heap[findIndex] = tmp; + index = findIndex; + } else { + break; + } + } + } +} +``` + +```python +# by chatGPT (python) +import heapq + +class Solution: + def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]: + # 存储三元组 (num1[i], nums2[i], i) + # i 记录 nums2 元素的索引位置,用于生成下一个节点 + pq = [] + for i in range(len(nums1)): + heapq.heappush(pq, [nums1[i], nums2[0], 0]) + + res = [] + # 执行合并多个有序链表的逻辑 + while pq and k > 0: + cur = heapq.heappop(pq) + k -= 1 + # 链表中的下一个节点加入优先级队列 + next_index = cur[2] + 1 + if next_index < len(nums2): + heapq.heappush(pq, [cur[0], nums2[next_index], next_index]) + + pair = [cur[0], cur[1]] + res.append(pair) + + return res +``` + +https://leetcode.cn/problems/find-k-pairs-with-smallest-sums 的多语言解法👆 + +https://leetcode.cn/problems/find-median-from-data-stream 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class MedianFinder { +private: + priority_queue large; // 小顶堆 + priority_queue, greater> small; // 大顶堆 + +public: + /** initialize your data structure here. */ + MedianFinder() { + + } + + void addNum(int num) { + if (small.size() >= large.size()) { + small.push(num); + large.push(small.top()); + small.pop(); + } else { + large.push(num); + small.push(large.top()); + large.pop(); + } + } + + double findMedian() { + /** + ![](../pictures/中位数/2.jpeg) + */ + // 如果元素不一样多,多的那个堆的堆顶元素就是中位数 + if (large.size() < small.size()) { + return small.top(); + } else if (large.size() > small.size()) { + return large.top(); + } + // 如果元素一样多,两个堆堆顶元素的平均数是中位数 + return (large.top() + small.top()) / 2.0; + /** + ![](../pictures/中位数/1.jpeg) + */ + } +}; +``` + +```go +// by chatGPT (go) +import "container/heap" + +type MedianFinder struct { + large *PriorityQueue + small *PriorityQueue +} + +// 小顶堆 +type PriorityQueue []int + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i] < pq[j] +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + *pq = append(*pq, x.(int)) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + x := old[n-1] + *pq = old[:n-1] + return x +} + +// 大顶堆 +type ReversePriorityQueue []int + +func (rpq ReversePriorityQueue) Len() int { + return len(rpq) +} + +func (rpq ReversePriorityQueue) Less(i, j int) bool { + return rpq[i] > rpq[j] +} + +func (rpq ReversePriorityQueue) Swap(i, j int) { + rpq[i], rpq[j] = rpq[j], rpq[i] +} + +func (rpq *ReversePriorityQueue) Push(x interface{}) { + *rpq = append(*rpq, x.(int)) +} + +func (rpq *ReversePriorityQueue) Pop() interface{} { + old := *rpq + n := len(old) + x := old[n-1] + *rpq = old[:n-1] + return x +} + +func Constructor() MedianFinder { + // 小顶堆 + large := &PriorityQueue{} + // 大顶堆 + small := &ReversePriorityQueue{} + + return MedianFinder{ + large: large, + small: small, + } +} + +func (this *MedianFinder) FindMedian() float64 { + // 如果元素不一样多,多的那个堆的堆顶元素就是中位数 + if this.large.Len() < this.small.Len() { + return float64(this.small.Top()) + } else if this.large.Len() > this.small.Len() { + return float64(this.large.Top()) + } + // 如果元素一样多,两个堆堆顶元素的平均数是中位数 + return (float64(this.large.Top()) + float64(this.small.Top())) / 2.0 +} + +func (this *MedianFinder) AddNum(num int) { + if this.small.Len() >= this.large.Len() { + this.small.Push(num) + heap.Push(this.large, this.small.Pop()) + } else { + this.large.Push(num) + heap.Push(this.small, this.large.Pop()) + } +} + +// 返回堆顶元素 +func (pq *PriorityQueue) Top() int { + return (*pq)[0] +} + +// 返回堆顶元素 +func (rpq *ReversePriorityQueue) Top() int { + return (*rpq)[0] +} +``` + +```java +// by labuladong (java) +class MedianFinder { + private PriorityQueue large; + private PriorityQueue small; + + public MedianFinder() { + // 小顶堆 + large = new PriorityQueue<>(); + // 大顶堆 + small = new PriorityQueue<>((a, b) -> { + return b - a; + }); + } + + public double findMedian() { + /** + ![](../pictures/中位数/2.jpeg) + */ + // 如果元素不一样多,多的那个堆的堆顶元素就是中位数 + if (large.size() < small.size()) { + return small.peek(); + } else if (large.size() > small.size()) { + return large.peek(); + } + // 如果元素一样多,两个堆堆顶元素的平均数是中位数 + return (large.peek() + small.peek()) / 2.0; + /** + ![](../pictures/中位数/1.jpeg) + */ + } + + public void addNum(int num) { + if (small.size() >= large.size()) { + small.offer(num); + large.offer(small.poll()); + } else { + large.offer(num); + small.offer(large.poll()); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var MedianFinder = function() { + // 小顶堆 + var large = new PriorityQueue(); + // 大顶堆 + var small = new PriorityQueue(function(a, b) { + return b - a; + }); + + this.findMedian = function() { + // 如果元素不一样多,多的那个堆的堆顶元素就是中位数 + if (large.size() < small.size()) { + return small.peek(); + } else if (large.size() > small.size()) { + return large.peek(); + } + // 如果元素一样多,两个堆堆顶元素的平均数是中位数 + return (large.peek() + small.peek()) / 2.0; + }; + + this.addNum = function(num) { + if (small.size() >= large.size()) { + small.offer(num); + large.offer(small.poll()); + } else { + large.offer(num); + small.offer(large.poll()); + } + }; +}; +``` + +```python +# by chatGPT (python) +class MedianFinder: + def __init__(self): + # 小顶堆 + self.large = [] + # 大顶堆 + self.small = [] + + def findMedian(self) -> float: + # 如果元素不一样多,多的那个堆的堆顶元素就是中位数 + if len(self.large) < len(self.small): + return float(self.small[0]) + elif len(self.large) > len(self.small): + return float(self.large[0]) + # 如果元素一样多,两个堆堆顶元素的平均数是中位数 + return (self.large[0] + self.small[0]) / 2.0 + + def addNum(self, num: int) -> None: + if len(self.small) >= len(self.large): + self.small.append(num) + self.large.append(-heapq.heappushpop(self.small, -self.large[0])) + else: + self.large.append(num) + self.small.append(-heapq.heappushpop(self.large, -self.small[0])) +``` + +https://leetcode.cn/problems/find-median-from-data-stream 的多语言解法👆 + +https://leetcode.cn/problems/find-mode-in-binary-search-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector mode; + TreeNode* prev = nullptr; + // 当前元素的重复次数 + int curCount = 0; + // 全局的最长相同序列长度 + int maxCount = 0; + + vector findMode(TreeNode* root) { + // 执行中序遍历 + traverse(root); + + vector res(mode.size()); + for (int i = 0; i < res.size(); i++) { + res[i] = mode[i]; + } + return res; + } + + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + traverse(root->left); + + // 中序遍历位置 + if (prev == nullptr) { + // 初始化 + curCount = 1; + maxCount = 1; + mode.push_back(root->val); + } else { + if (root->val == prev->val) { + // root.val 重复的情况 + curCount++; + if (curCount == maxCount) { + // root.val 是众数 + mode.push_back(root->val); + } else if (curCount > maxCount) { + // 更新众数 + mode.clear(); + maxCount = curCount; + mode.push_back(root->val); + } + } + + if (root->val != prev->val) { + // root.val 不重复的情况 + curCount = 1; + if (curCount == maxCount) { + mode.push_back(root->val); + } + } + } + // 别忘了更新 prev + prev = root; + + traverse(root->right); + } +}; +``` + +```go +// by chatGPT (go) +func findMode(root *TreeNode) []int { + var mode []int + var prev *TreeNode + var curCount, maxCount int + + traverse := func(node *TreeNode) { + if node == nil { + return + } + traverse(node.Left) + + // 中序遍历位置 + if prev == nil { + // 初始化 + curCount = 1 + maxCount = 1 + mode = append(mode, node.Val) + } else { + if node.Val == prev.Val { + // node.Val 重复的情况 + curCount++ + if curCount == maxCount { + // node.Val 是众数 + mode = append(mode, node.Val) + } else if curCount > maxCount { + // 更新众数 + mode = make([]int, 0) + maxCount = curCount + mode = append(mode, node.Val) + } + } + + if node.Val != prev.Val { + // node.Val 不重复的情况 + curCount = 1 + if curCount == maxCount { + mode = append(mode, node.Val) + } + } + } + // 别忘了更新 prev + prev = node + + traverse(node.Right) + } + + traverse(root) + + return mode +} +``` + +```java +// by labuladong (java) +class Solution { + ArrayList mode = new ArrayList<>(); + TreeNode prev = null; + // 当前元素的重复次数 + int curCount = 0; + // 全局的最长相同序列长度 + int maxCount = 0; + + public int[] findMode(TreeNode root) { + // 执行中序遍历 + traverse(root); + + int[] res = new int[mode.size()]; + for (int i = 0; i < res.length; i++) { + res[i] = mode.get(i); + } + return res; + } + + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.left); + + // 中序遍历位置 + if (prev == null) { + // 初始化 + curCount = 1; + maxCount = 1; + mode.add(root.val); + } else { + if (root.val == prev.val) { + // root.val 重复的情况 + curCount++; + if (curCount == maxCount) { + // root.val 是众数 + mode.add(root.val); + } else if (curCount > maxCount) { + // 更新众数 + mode.clear(); + maxCount = curCount; + mode.add(root.val); + } + } + + if (root.val != prev.val) { + // root.val 不重复的情况 + curCount = 1; + if (curCount == maxCount) { + mode.add(root.val); + } + } + } + // 别忘了更新 prev + prev = root; + + traverse(root.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +var findMode = function(root) { + let mode = []; + let prev = null; + // 当前元素的重复次数 + let curCount = 0; + // 全局的最长相同序列长度 + let maxCount = 0; + + function traverse(root) { + if (root === null) { + return; + } + traverse(root.left); + + // 中序遍历位置 + if (prev === null) { + // 初始化 + curCount = 1; + maxCount = 1; + mode.push(root.val); + } else { + if (root.val === prev.val) { + // root.val 重复的情况 + curCount++; + if (curCount === maxCount) { + // root.val 是众数 + mode.push(root.val); + } else if (curCount > maxCount) { + // 更新众数 + mode = [root.val]; + maxCount = curCount; + } + } + + if (root.val !== prev.val) { + // root.val 不重复的情况 + curCount = 1; + if (curCount === maxCount) { + mode.push(root.val); + } + } + } + // 别忘了更新 prev + prev = root; + + traverse(root.right); + } + + traverse(root); + + return mode; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.mode = [] + self.prev = None + # 当前元素的重复次数 + self.curCount = 0 + # 全局的最长相同序列长度 + self.maxCount = 0 + + def findMode(self, root: TreeNode) -> List[int]: + # 执行中序遍历 + self.traverse(root) + + return self.mode + + def traverse(self, root: TreeNode) -> None: + if not root: + return + self.traverse(root.left) + + # 中序遍历位置 + if not self.prev: + # 初始化 + self.curCount = 1 + self.maxCount = 1 + self.mode.append(root.val) + else: + if root.val == self.prev.val: + # root.val 重复的情况 + self.curCount += 1 + if self.curCount == self.maxCount: + # root.val 是众数 + self.mode.append(root.val) + elif self.curCount > self.maxCount: + # 更新众数 + self.mode.clear() + self.maxCount = self.curCount + self.mode.append(root.val) + + if root.val != self.prev.val: + # root.val 不重复的情况 + self.curCount = 1 + if self.curCount == self.maxCount: + self.mode.append(root.val) + + # 别忘了更新 prev + self.prev = root + self.traverse(root.right) +``` + +https://leetcode.cn/problems/find-mode-in-binary-search-tree 的多语言解法👆 + +https://leetcode.cn/problems/first-unique-character-in-a-string 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int firstUniqChar(string s) { + int count[26] = {0}; + for (char c : s) { + // 将字符转化成数字 + count[c - 'a']++; + } + for (int i = 0; i < s.length(); i++) { + char c = s[i]; + if (count[c - 'a'] == 1) { + // 第一个出现一次的字符 + return i; + } + } + return -1; + } +}; +``` + +```go +// by chatGPT (go) +func firstUniqChar(s string) int { + // 创建一个大小为26的整数数组count + count := make([]int, 26) + // 遍历字符串s的每一个字符 + for _, c := range s { + // 将字符转化成数字 + count[c-'a']++ + } + // 遍历字符串s的每一个字符 + for i, c := range s { + // 找到第一个出现一次的字符 + if count[c-'a'] == 1 { + return i + } + } + // 没有出现一次的字符 + return -1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int firstUniqChar(String s) { + int[] count = new int[26]; + for (char c : s.toCharArray()) { + // 将字符转化成数字 + count[c - 'a']++; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (count[c - 'a'] == 1) { + // 第一个出现一次的字符 + return i; + } + } + return -1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var firstUniqChar = function(s) { + let count = new Array(26).fill(0); + for (let i = 0; i < s.length; i++) { + // 将字符转化成数字 + count[s.charCodeAt(i) - 97]++; + } + for (let i = 0; i < s.length; i++) { + if (count[s.charCodeAt(i) - 97] === 1) { + // 第一个出现一次的字符 + return i; + } + } + return -1; +} +``` + +```python +# by chatGPT (python) +class Solution: + def firstUniqChar(self, s: str) -> int: + count = [0]*26 + for c in s: + # 将字符转化成数字 + count[ord(c) - ord('a')] += 1 + for i in range(len(s)): + c = s[i] + if count[ord(c) - ord('a')] == 1: + # 第一个出现一次的字符 + return i + return -1 +``` + +https://leetcode.cn/problems/first-unique-character-in-a-string 的多语言解法👆 + +https://leetcode.cn/problems/flatten-binary-tree-to-linked-list 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 定义:将以 root 为根的树拉平为链表 + void flatten(TreeNode* root) { + // base case + if (root == nullptr) return; + // 先递归拉平左右子树 + flatten(root->left); + flatten(root->right); + + /****后序遍历位置****/ + // 1、左右子树已经被拉平成一条链表 + TreeNode* left = root->left; + TreeNode* right = root->right; + + // 2、将左子树作为右子树 + root->left = nullptr; + root->right = left; + + // 3、将原先的右子树接到当前右子树的末端 + TreeNode* p = root; + while (p->right != nullptr) { + p = p->right; + } + p->right = right; + /** + ![](../pictures/二叉树系列/2.jpeg) + */ + } +}; +``` + +```go +// by chatGPT (go) +// 定义一个 TreeNode 结构体 + +// 定义解题函数 +func flatten(root *TreeNode) { + // base case + if root == nil { + return + } + // 先递归拉平左右子树 + flatten(root.Left) + flatten(root.Right) + + /****后序遍历位置****/ + // 1、左右子树已经被拉平成一条链表 + left := root.Left + right := root.Right + + // 2、将左子树作为右子树 + root.Left = nil + root.Right = left + + // 3、将原先的右子树接到当前右子树的末端 + p := root + for p.Right != nil { + p = p.Right + } + p.Right = right +} +``` + +```java +// by labuladong (java) +class Solution { + // 定义:将以 root 为根的树拉平为链表 + public void flatten(TreeNode root) { + // base case + if (root == null) return; + // 先递归拉平左右子树 + flatten(root.left); + flatten(root.right); + + /****后序遍历位置****/ + // 1、左右子树已经被拉平成一条链表 + TreeNode left = root.left; + TreeNode right = root.right; + + // 2、将左子树作为右子树 + root.left = null; + root.right = left; + + // 3、将原先的右子树接到当前右子树的末端 + TreeNode p = root; + while (p.right != null) { + p = p.right; + } + p.right = right; + /** + ![](../pictures/二叉树系列/2.jpeg) + */ + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {void} Do not return anything, modify root in-place instead. + */ +var flatten = function(root) { + // base case + if (root == null) return; + // 先递归拉平左右子树 + flatten(root.left); + flatten(root.right); + + /****后序遍历位置****/ + // 1、左右子树已经被拉平成一条链表 + var left = root.left; + var right = root.right; + + // 2、将左子树作为右子树 + root.left = null; + root.right = left; + + // 3、将原先的右子树接到当前右子树的末端 + var p = root; + while (p.right != null) { + p = p.right; + } + p.right = right; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def flatten(self, root: TreeNode) -> None: + """ + Do not return anything, modify root in-place instead. + """ + # base case + if not root: + return + + # 先递归拉平左右子树 + self.flatten(root.left) + self.flatten(root.right) + + # 后序遍历位置 + # 1、左右子树已经被拉平成一条链表 + left = root.left + right = root.right + + # 2、将左子树作为右子树 + root.left = None + root.right = left + + # 3、将原先的右子树接到当前右子树的末端 + p = root + while p.right: + p = p.right + p.right = right +``` + +https://leetcode.cn/problems/flatten-binary-tree-to-linked-list 的多语言解法👆 + +https://leetcode.cn/problems/flatten-nested-list-iterator 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class NestedIterator { +private: + std::deque list; + +public: + NestedIterator(std::vector &nestedList) { + // 不直接用 nestedList 的引用,是因为不能确定它的底层实现 + // 必须保证是 deque,否则下面的 push_front 会很低效 + list = std::deque(nestedList.begin(), nestedList.end()); + } + + int next() { + // hasNext 方法保证了第一个元素一定是整数类型 + int val = list.front().getInteger(); + list.pop_front(); + return val; + } + + bool hasNext() { + // 循环拆分列表元素,直到列表第一个元素是整数类型 + while (!list.empty() && !list.front().isInteger()) { + // 当列表开头第一个元素是列表类型时,进入循环 + std::vector first = list.front().getList(); + // 将第一个列表打平并按顺序添加到开头 + list.pop_front(); + for (int i = first.size() - 1; i >= 0; i--) { + list.push_front(first[i]); + } + } + return !list.empty(); + } +}; +``` + +```go +// by chatGPT (go) +type NestedIterator struct { + list []NestedInteger +} + +func Constructor(nestedList []*NestedInteger) *NestedIterator { + list := make([]NestedInteger, 0) + for _, nestedInt := range nestedList { + list = append(list, *nestedInt) + } + return &NestedIterator{list} +} + +func (this *NestedIterator) Next() int { + // hasNext 方法保证了第一个元素一定是整数类型 + res := this.list[0].GetInteger() + this.list = this.list[1:] + return res +} + +func (this *NestedIterator) HasNext() bool { + // 循环拆分列表元素,直到列表第一个元素是整数类型 + for len(this.list) > 0 && !this.list[0].IsInteger() { + // 当列表开头第一个元素是列表类型时,进入循环 + first := this.list[0].GetList() + this.list = this.list[1:] + // 将第一个列表打平并按顺序添加到开头 + for i := len(first) - 1; i >= 0; i-- { + this.list = append([]NestedInteger{first[i]}, this.list...) + } + } + return len(this.list) > 0 +} +``` + +```java +// by labuladong (java) +public class NestedIterator implements Iterator { + private LinkedList list; + + public NestedIterator(List nestedList) { + // 不直接用 nestedList 的引用,是因为不能确定它的底层实现 + // 必须保证是 LinkedList,否则下面的 addFirst 会很低效 + list = new LinkedList<>(nestedList); + } + + public Integer next() { + // hasNext 方法保证了第一个元素一定是整数类型 + return list.remove(0).getInteger(); + } + + public boolean hasNext() { + // 循环拆分列表元素,直到列表第一个元素是整数类型 + while (!list.isEmpty() && !list.get(0).isInteger()) { + // 当列表开头第一个元素是列表类型时,进入循环 + List first = list.remove(0).getList(); + // 将第一个列表打平并按顺序添加到开头 + for (int i = first.size() - 1; i >= 0; i--) { + list.addFirst(first.get(i)); + } + } + return !list.isEmpty(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @constructor + * @param {Array} nestedList + */ +var NestedIterator = function(nestedList) { + // 不直接用 nestedList 的引用,是因为不能确定它的底层实现 + // 必须保证是 LinkedList,否则下面的 addFirst 会很低效 + this.list = new LinkedList(nestedList); +}; + +/** + * @this NestedIterator + * @returns {number} + */ +NestedIterator.prototype.next = function() { + // hasNext 方法保证了第一个元素一定是整数类型 + return this.list.remove(0).getInteger(); +}; + +/** + * @this NestedIterator + * @returns {boolean} + */ +NestedIterator.prototype.hasNext = function() { + // 循环拆分列表元素,直到列表第一个元素是整数类型 + while (!this.list.isEmpty() && !this.list.get(0).isInteger()) { + // 当列表开头第一个元素是列表类型时,进入循环 + const first = this.list.remove(0).getList(); + // 将第一个列表打平并按顺序添加到开头 + for (let i = first.length - 1; i >= 0; i--) { + this.list.addFirst(first[i]); + } + } + return !this.list.isEmpty(); +}; +``` + +```python +# by chatGPT (python) +class NestedIterator: + def __init__(self, nestedList: [NestedInteger]): + # 不直接用 nestedList 的引用,是因为不能确定它的底层实现 + # 必须保证是 LinkedList,否则下面的 addFirst 会很低效 + self.list = deque(nestedList) + + def next(self) -> int: + # hasNext 方法保证了第一个元素一定是整数类型 + return self.list.popleft().getInteger() + + def hasNext(self) -> bool: + # 循环拆分列表元素,直到列表第一个元素是整数类型 + while self.list and not self.list[0].isInteger(): + # 当列表开头第一个元素是列表类型时,进入循环 + first = self.list.popleft().getList() + # 将第一个列表打平并按顺序添加到开头 + for i in range(len(first)-1, -1, -1): + self.list.appendleft(first[i]) + return bool(self.list) +``` + +https://leetcode.cn/problems/flatten-nested-list-iterator 的多语言解法👆 + +https://leetcode.cn/problems/flip-game 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector generatePossibleNextMoves(string currentState) { + vector res; + for (int i = 1; i < currentState.size(); i++) { + if (currentState[i] == '+' && currentState[i - 1] == '+') { + // 做选择 + currentState[i] = '-'; + currentState[i - 1] = '-'; + res.push_back(currentState); + // 撤销选择 + currentState[i] = '+'; + currentState[i - 1] = '+'; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func generatePossibleNextMoves(currentState string) []string { + res := []string{} + arr := []rune(currentState) + for i := 1; i < len(arr); i++ { + if arr[i] == '+' && arr[i-1] == '+' { + // 做选择 + arr[i] = '-' + arr[i-1] = '-' + res = append(res, string(arr)) + // 撤销选择 + arr[i] = '+' + arr[i-1] = '+' + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List generatePossibleNextMoves(String currentState) { + List res = new ArrayList<>(); + char[] arr = currentState.toCharArray(); + for (int i = 1; i < arr.length; i++) { + if (arr[i] == '+' && arr[i - 1] == '+') { + // 做选择 + arr[i] = '-'; + arr[i - 1] = '-'; + res.add(new String(arr)); + // 撤销选择 + arr[i] = '+'; + arr[i - 1] = '+'; + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var generatePossibleNextMoves = function(currentState) { + var res = []; + var arr = Array.from(currentState); + for (var i = 1; i < arr.length; i++) { + if (arr[i] === '+' && arr[i - 1] === '+') { + // 做选择 + arr[i] = '-'; + arr[i - 1] = '-'; + res.push(arr.join('')); + // 撤销选择 + arr[i] = '+'; + arr[i - 1] = '+'; + } + } + return res; +} +``` + +```python +# by chatGPT (python) +class Solution(object): + def generatePossibleNextMoves(self, currentState): + """ + :type currentState: str + :rtype: List[str] + """ + res = [] + for i in range(1, len(currentState)): + if currentState[i] == '+' and currentState[i - 1] == '+': + # 做选择 + res.append(currentState[:i - 1] + '--' + currentState[i + 1:]) + # 撤销选择 is not needed in python due to its immutability of string + return res +``` + +https://leetcode.cn/problems/flip-game 的多语言解法👆 + +https://leetcode.cn/problems/flip-game-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 直接把 293 的代码 copy 过来,生成所有可能的下一步 + vector generatePossibleNextMoves(string currentState) { + vector res; + for (int i = 1; i < currentState.size(); i++) { + if (currentState[i] == '+' && currentState[i - 1] == '+') { + // 做选择 + currentState[i] = '-'; + currentState[i - 1] = '-'; + res.push_back(currentState); + // 撤销选择 + currentState[i] = '+'; + currentState[i - 1] = '+'; + } + } + return res; + } + + // 备忘录 + unordered_map memo; + + bool canWin(string currentState) { + // 记入备忘录 + memo.clear(); + return dp(currentState); + } + + // 定义:输入字符串 s,返回在此局面下先手是否可能赢 + bool dp(string s) { + if (memo.find(s) != memo.end()) { + // 之前遇到过这种局面,直接返回结果 + return memo[s]; + } + vector nextStates = generatePossibleNextMoves(s); + bool res = false; + for (string nextState : nextStates) { + // 只要有一个是 false,说明当前玩家赢了 + bool win = dp(nextState); + // 后序位置,做判断 + if (!win) { + res = true; + break; + } + } + // 记入备忘录 + memo[s] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +import "strings" + +// 直接把 293 的代码 copy 过来,生成所有可能的下一步 +func generatePossibleNextMoves(currentState string) []string { + res := make([]string, 0) + arr := []rune(currentState) + for i := 1; i < len(arr); i++ { + if arr[i] == '+' && arr[i-1] == '+' { + // making a choice + // 做选择 + arr[i] = '-' + arr[i-1] = '-' + res = append(res, string(arr)) + // undo choice + // 撤销选择 + arr[i] = '+' + arr[i-1] = '+' + } + } + return res +} + +func canWin(currentState string) bool { + // Definition: Enter string s, return whether the first hand can win in this face. + // 定义:输入字符串 s,返回在此局面下先手是否可能赢 + var dp func(string, map[string]bool) bool + dp = func(s string, memo map[string]bool) bool { + if _, ok := memo[s]; ok { + // have encountered this position before, return the result directly + // 之前遇到过这种局面,直接返回结果 + return memo[s] + } + res := false + nextStates := generatePossibleNextMoves(s) + for _, nextState := range nextStates { + // As long as one is false, it means the current player won + // 只要有一个是 false,说明当前玩家赢了 + win := dp(nextState, memo) + // subsequent position, make a judgment + // 后序位置,做判断 + if !win { + res = true + break + } + } + // record in the memo + // 记入备忘录 + memo[s] = res + return res + } + + // memo is a map that acts as the memoization for dp. + // 备忘录 + memo := make(map[string]bool) + return dp(currentState, memo) +} +``` + +```java +// by labuladong (java) +class Solution { + + // 直接把 293 的代码 copy 过来,生成所有可能的下一步 + List generatePossibleNextMoves(String currentState) { + List res = new ArrayList<>(); + char[] arr = currentState.toCharArray(); + for (int i = 1; i < arr.length; i++) { + if (arr[i] == '+' && arr[i - 1] == '+') { + // 做选择 + arr[i] = '-'; + arr[i - 1] = '-'; + res.add(new String(arr)); + // 撤销选择 + arr[i] = '+'; + arr[i - 1] = '+'; + } + } + return res; + } + + // 备忘录 + Map memo = new HashMap<>(); + + public boolean canWin(String currentState) { + // 记入备忘录 + memo.clear(); + return dp(currentState); + } + + // 定义:输入字符串 s,返回在此局面下先手是否可能赢 + boolean dp(String s) { + if (memo.containsKey(s)) { + // 之前遇到过这种局面,直接返回结果 + return memo.get(s); + } + boolean res = false; + List nextStates = generatePossibleNextMoves(s); + for (String nextState : nextStates) { + // 只要有一个是 false,说明当前玩家赢了 + boolean win = dp(nextState); + // 后序位置,做判断 + if (!win) { + res = true; + break; + } + } + // 记入备忘录 + memo.put(s, res); + return res; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + + # 直接把 293 的代码 copy 过来,生成所有可能的下一步 + def generatePossibleNextMoves(self, currentState): + res = [] + arr = list(currentState) + for i in range(1, len(arr)): + if arr[i] == '+' and arr[i - 1] == '+': + # 做选择 + arr[i] = '-' + arr[i - 1] = '-' + res.append("".join(arr)) + # 撤销选择 + arr[i] = '+' + arr[i - 1] = '+' + return res + + # 备忘录 + memo = {} + + def canWin(self, currentState): + # 记入备忘录 + self.memo.clear() + return self.dp(currentState) + + # 定义:输入字符串 s,返回在此局面下先手是否可能赢 + def dp(self, s): + if s in self.memo: + # 之前遇到过这种局面,直接返回结果 + return self.memo[s] + res = False + nextStates = self.generatePossibleNextMoves(s) + for nextState in nextStates: + # 只要有一个是 false,说明当前玩家赢了 + win = self.dp(nextState) + # 后序位置,做判断 + if not win: + res = True + break + # 记入备忘录 + self.memo[s] = res + return res +``` + +https://leetcode.cn/problems/flip-game-ii 的多语言解法👆 + +https://leetcode.cn/problems/freedom-trail 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + // 字符 -> 索引列表 + unordered_map> charToIndex; + // 备忘录 + vector> memo; + + /* 主函数 */ + int findRotateSteps(string ring, string key) { + int m = ring.size(); + int n = key.size(); + // 备忘录全部初始化为 0 + memo.resize(m, vector(n, 0)); + // 记录圆环上字符到索引的映射 + for (int i = 0; i < ring.size(); i++) { + charToIndex[ring[i]].push_back(i); + } + // 圆盘指针最初指向 12 点钟方向, + // 从第一个字符开始输入 key + return dp(ring, 0, key, 0); + } + + // 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数 + int dp(string& ring, int i, string& key, int j) { + // base case 完成输入 + if (j == key.size()) return 0; + // 查找备忘录,避免重叠子问题 + if (memo[i][j] != 0) return memo[i][j]; + + int n = ring.size(); + // 做选择 + int res = INT_MAX; + // ring 上可能有多个字符 key[j] + for (int k : charToIndex[key[j]]) { + // 拨动指针的次数 + int delta = abs(k - i); + // 选择顺时针还是逆时针 + delta = min(delta, n - delta); + // 将指针拨到 ring[k],继续输入 key[j+1..] + int subProblem = dp(ring, k, key, j + 1); + // 选择「整体」操作次数最少的 + // 加一是因为按动按钮也是一次操作 + res = min(res, 1 + delta + subProblem); + } + // 将结果存入备忘录 + memo[i][j] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +// 计算两个数相减的绝对值 +func abs(a, b int) int { + if a > b { + return b - a + 2*b + } else { + return a - b + 2*b + } +} + +// 返回两个数的最小值 +func min(a, b int) int { + if a < b { + return a + } else { + return b + } +} + +/* 主函数 */ +func findRotateSteps(ring string, key string) int { + m := len(ring) + n := len(key) + // 备忘录全部初始化为 0 + memo := make([][]int, m) + for i := range memo { + memo[i] = make([]int, n) + } + // 记录圆环上字符到索引的映射 + charToIndex := make(map[byte][]int) + for i := range ring { + charToIndex[ring[i]] = append(charToIndex[ring[i]], i) + } + // 圆盘指针最初指向 12 点钟方向, + // 从第一个字符开始输入 key + return dp(ring, 0, key, 0, charToIndex, memo) +} + +// 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数 +func dp(ring string, i int, key string, j int, charToIndex map[byte][]int, memo [][]int) int { + // base case 完成输入 + if j == len(key) { + return 0 + } + // 查找备忘录,避免重叠子问题 + if memo[i][j] != 0 { + return memo[i][j] + } + + n := len(ring) + // 做选择 + res := math.MaxInt32 + // ring 上可能有多个字符 key[j] + for _, k := range charToIndex[key[j]] { + // 拨动指针的次数 + delta := abs(k-i, n) + // 选择顺时针还是逆时针 + delta = min(delta, n-delta) + // 将指针拨到 ring[k],继续输入 key[j+1..] + subProblem := dp(ring, k, key, j+1, charToIndex, memo) + // 选择「整体」操作次数最少的 + // 加一是因为按动按钮也是一次操作 + res = min(res, 1+delta+subProblem) + } + // 将结果存入备忘录 + memo[i][j] = res + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + // 字符 -> 索引列表 + Map> charToIndex = new HashMap<>(); + // 备忘录 + int[][] memo; + + /* 主函数 */ + public int findRotateSteps(String ring, String key) { + int m = ring.length(); + int n = key.length(); + // 备忘录全部初始化为 0 + memo = new int[m][n]; + for (int[] row : memo) { + Arrays.fill(row, 0); + } + // 记录圆环上字符到索引的映射 + for (int i = 0; i < ring.length(); i++) { + char c = ring.charAt(i); + List list = charToIndex.getOrDefault(c, new ArrayList<>()); + list.add(i); + charToIndex.put(c, list); + } + // 圆盘指针最初指向 12 点钟方向, + // 从第一个字符开始输入 key + return dp(ring, 0, key, 0); + } + + // 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数 + private int dp(String ring, int i, String key, int j) { + // base case 完成输入 + if (j == key.length()) return 0; + // 查找备忘录,避免重叠子问题 + if (memo[i][j] != 0) return memo[i][j]; + + int n = ring.length(); + // 做选择 + int res = Integer.MAX_VALUE; + char c = key.charAt(j); + // ring 上可能有多个字符 key[j] + for (int k : charToIndex.getOrDefault(c, new ArrayList<>())) { + // 拨动指针的次数 + int delta = Math.abs(k - i); + // 选择顺时针还是逆时针 + delta = Math.min(delta, n - delta); + // 将指针拨到 ring[k],继续输入 key[j+1..] + int subProblem = dp(ring, k, key, j + 1); + // 选择「整体」操作次数最少的 + // 加一是因为按动按钮也是一次操作 + res = Math.min(res, 1 + delta + subProblem); + } + // 将结果存入备忘录 + memo[i][j] = res; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findRotateSteps = function(ring, key) { + // 字符 -> 索引列表 + const charToIndex = {}; + // 备忘录 + const memo = []; + + /* 记录圆环上字符到索引的映射 */ + for (let i = 0; i < ring.length; i++) { + const char = ring.charAt(i); + if (charToIndex[char] === undefined) { + charToIndex[char] = []; + } + charToIndex[char].push(i); + } + + /* 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数 */ + function dp(i, j) { + // base case 完成输入 + if (j === key.length) { + return 0; + } + // 查找备忘录,避免重叠子问题 + if (memo[i] !== undefined && memo[i][j] !== undefined) { + return memo[i][j]; + } + + const n = ring.length; + // 做选择 + let res = Infinity; + // ring 上可能有多个字符 key[j] + for (let k of charToIndex[key.charAt(j)]) { + // 拨动指针的次数 + let delta = Math.abs(k - i); + // 选择顺时针还是逆时针 + delta = Math.min(delta, n - delta); + // 将指针拨到 ring[k],继续输入 key[j+1..] + const subProblem = dp(k, j + 1); + // 选择「整体」操作次数最少的 + // 加一是因为按动按钮也是一次操作 + res = Math.min(res, 1 + delta + subProblem); + } + // 将结果存入备忘录 + if (memo[i] === undefined) { + memo[i] = []; + } + memo[i][j] = res; + return res; + } + + return dp(0, 0); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + # 字符 -> 索引列表 + self.charToIndex = {} + # 备忘录 + self.memo = [] + + # 主函数 + def findRotateSteps(self, ring: str, key: str) -> int: + m = len(ring) + n = len(key) + # 备忘录全部初始化为 0 + self.memo = [[0] * n for _ in range(m)] + # 记录圆环上字符到索引的映射 + for i in range(m): + char = ring[i] + if char in self.charToIndex: + self.charToIndex[char].append(i) + else: + self.charToIndex[char] = [i] + # 圆盘指针最初指向 12 点钟方向, + # 从第一个字符开始输入 key + return self.dp(ring, 0, key, 0) + + # 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数 + def dp(self, ring: str, i: int, key: str, j: int) -> int: + # base case 完成输入 + if j == len(key): + return 0 + # 查找备忘录,避免重叠子问题 + if self.memo[i][j] != 0: + return self.memo[i][j] + + n = len(ring) + # 做选择 + res = float('inf') + # ring 上可能有多个字符 key[j] + for k in self.charToIndex[key[j]]: + # 拨动指针的次数 + delta = abs(k - i) + # 选择顺时针还是逆时针 + delta = min(delta, n - delta) + # 将指针拨到 ring[k],继续输入 key[j+1..] + subProblem = self.dp(ring, k, key, j + 1) + # 选择「整体」操作次数最少的 + # 加一是因为按动按钮也是一次操作 + res = min(res, 1 + delta + subProblem) + + # 将结果存入备忘录 + self.memo[i][j] = res + return res +``` + +https://leetcode.cn/problems/freedom-trail 的多语言解法👆 + +https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + Node* copyRandomList(Node* head) { + unordered_map originToClone; + // 第一次遍历,先把所有节点克隆出来 + for (Node* p = head; p != nullptr; p = p->next) { + if (originToClone.find(p) == originToClone.end()) { + originToClone[p] = new Node(p->val); + } + } + // 第二次遍历,把克隆节点的结构连接好 + for (Node* p = head; p != nullptr; p = p->next) { + if (p->next != nullptr) { + originToClone[p]->next = originToClone[p->next]; + } + if (p->random != nullptr) { + originToClone[p]->random = originToClone[p->random]; + } + } + // 返回克隆之后的头结点 + return originToClone[head]; + } +}; + +// 用递归的方式进行遍历 +class Solution2 { +public: + Node* copyRandomList(Node* head) { + traverse(head); + return originToClone[head]; + } + +private: + // 记录 DFS 遍历过的节点,防止走回头路 + unordered_set visited; + // 记录原节点到克隆节点的映射 + unordered_map originToClone; + + // DFS 图遍历框架 + void traverse(Node* node) { + if (node == nullptr) { + return; + } + if (visited.find(node) != visited.end()) { + return; + } + // 前序位置,标记为已访问 + visited.insert(node); + // 前序位置,克隆节点 + if (originToClone.find(node) == originToClone.end()) { + originToClone[node] = new Node(node->val); + } + Node* cloneNode = originToClone[node]; + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + + traverse(node->next); + cloneNode->next = originToClone[node->next]; + + traverse(node->random); + cloneNode->random = originToClone[node->random]; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a Node. + * type Node struct { + * Val int + * Next *Node + * Random *Node + * } + */ + +func copyRandomList(head *Node) *Node { + originToClone := make(map[*Node]*Node) + // 第一次遍历,先把所有节点克隆出来 + for p := head; p != nil; p = p.Next { + if _, ok := originToClone[p]; !ok { + originToClone[p] = &Node{Val:p.Val} + } + } + // 第二次遍历,把克隆节点的结构连接好 + for p := head; p != nil; p = p.Next { + if p.Next != nil { + originToClone[p].Next = originToClone[p.Next] + } + if p.Random != nil { + originToClone[p].Random = originToClone[p.Random] + } + } + // 返回克隆之后的头结点 + return originToClone[head] +} + +// 用递归的方式进行遍历 +func copyRandomList2(head *Node) *Node { + if head == nil { + return nil + } + visited := make(map[*Node]bool) + originToClone := make(map[*Node]*Node) + traverse(head, visited, originToClone) + return originToClone[head] +} + +// DFS 图遍历框架 +func traverse(node *Node, visited map[*Node]bool, originToClone map[*Node]*Node) { + if visited[node] { + return + } + // 前序位置,标记为已访问 + visited[node] = true + // 前序位置,克隆节点 + if _, ok := originToClone[node]; !ok { + originToClone[node] = &Node{Val:node.Val} + } + cloneNode := originToClone[node] + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + if node.Next != nil { + traverse(node.Next, visited, originToClone) + cloneNode.Next = originToClone[node.Next] + } + if node.Random != nil { + traverse(node.Random, visited, originToClone) + cloneNode.Random = originToClone[node.Random] + } +} +``` + +```java +// by labuladong (java) +class Solution { + public Node copyRandomList(Node head) { + HashMap originToClone = new HashMap<>(); + // 第一次遍历,先把所有节点克隆出来 + for (Node p = head; p != null; p = p.next) { + if (!originToClone.containsKey(p)) { + originToClone.put(p, new Node(p.val)); + } + } + // 第二次遍历,把克隆节点的结构连接好 + for (Node p = head; p != null; p = p.next) { + if (p.next != null) { + originToClone.get(p).next = originToClone.get(p.next); + } + if (p.random != null) { + originToClone.get(p).random = originToClone.get(p.random); + } + } + // 返回克隆之后的头结点 + return originToClone.get(head); + } +} + +// 用递归的方式进行遍历 +class Solution2 { + public Node copyRandomList(Node head) { + traverse(head); + return originToClone.get(head); + } + + // 记录 DFS 遍历过的节点,防止走回头路 + HashSet visited = new HashSet<>(); + // 记录原节点到克隆节点的映射 + HashMap originToClone = new HashMap<>(); + + // DFS 图遍历框架 + void traverse(Node node) { + if (node == null) { + return; + } + if (visited.contains(node)) { + return; + } + // 前序位置,标记为已访问 + visited.add(node); + // 前序位置,克隆节点 + if (!originToClone.containsKey(node)) { + originToClone.put(node, new Node(node.val)); + } + Node cloneNode = originToClone.get(node); + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + + traverse(node.next); + cloneNode.next = originToClone.get(node.next); + + traverse(node.random); + cloneNode.random = originToClone.get(node.random); + } +} +``` + +```javascript +// by chatGPT (javascript) +var copyRandomList = function(head) { + const originToClone = new Map(); + + // 第一次遍历,先把所有节点克隆出来 + for (let p = head; p != null; p = p.next) { + if (!originToClone.has(p)) { + originToClone.set(p, new Node(p.val)); + } + } + + // 第二次遍历,把克隆节点的结构连接好 + for (let p = head; p != null; p = p.next) { + if (p.next != null) { + originToClone.get(p).next = originToClone.get(p.next); + } + if (p.random != null) { + originToClone.get(p).random = originToClone.get(p.random); + } + } + + // 返回克隆之后的头结点 + return originToClone.get(head); +}; + +// 用递归的方式进行遍历 +var copyRandomList2 = function(head) { + const visited = new Set(); + const originToClone = new Map(); + + // DFS 图遍历框架 + const traverse = (node) => { + if (node == null) { + return; + } + if (visited.has(node)) { + return; + } + + // 前序位置,标记为已访问 + visited.add(node); + + // 前序位置,克隆节点 + if (!originToClone.has(node)) { + originToClone.set(node, new Node(node.val)); + } + + const cloneNode = originToClone.get(node); + + // 递归遍历邻居节点,并构建克隆图 + // 递归之后,邻居节点一定存在 originToClone 中 + + traverse(node.next); + cloneNode.next = originToClone.get(node.next); + + traverse(node.random); + cloneNode.random = originToClone.get(node.random); + }; + + traverse(head); + return originToClone.get(head); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def copyRandomList(self, head: 'Node') -> 'Node': + originToClone = {} + # 第一次遍历,先把所有节点克隆出来 + for p in [head]: + while p: + if p not in originToClone: + originToClone[p] = Node(p.val) + p = p.next + # 第二次遍历,把克隆节点的结构连接好 + for p in [head]: + while p: + if p.next: + originToClone[p].next = originToClone[p.next] + if p.random: + originToClone[p].random = originToClone[p.random] + p = p.next + # 返回克隆之后的头结点 + return originToClone.get(head) + +class Solution2: + def copyRandomList(self, head: 'Node') -> 'Node': + self.visited = set() + self.originToClone = {} + self.traverse(head) + return self.originToClone.get(head) + + # DFS 图遍历框架 + def traverse(self, node): + if not node: + return + if node in self.visited: + return + # 前序位置,标记为已访问 + self.visited.add(node) + # 前序位置,克隆节点 + if node not in self.originToClone: + self.originToClone[node] = Node(node.val) + cloneNode = self.originToClone[node] + + # 递归遍历邻居节点,并构建克隆图 + # 递归之后,邻居节点一定存在 originToClone 中 + + self.traverse(node.next) + cloneNode.next = self.originToClone.get(node.next) + + self.traverse(node.random) + cloneNode.random = self.originToClone.get(node.random) +``` + +https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/g5c51o 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 用优先级队列解决这道题 +class Solution { +public: + vector topKFrequent(vector& nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + unordered_map valToFreq; + for (int v : nums) { + valToFreq[v]++; + } + + priority_queue, vector>, greater>> pq; + // 队列按照键值对中的值(元素出现频率)从小到大排序 + for (auto entry : valToFreq) { + pq.push(make_pair(entry.second, entry.first)); + if (pq.size() > k) { + // 弹出最小元素,维护队列内是 k 个频率最大的元素 + pq.pop(); + } + } + + vector res(k); + for (int i = 0; i < k; i++) { + // res 数组中存储前 k 个最大元素 + res[i] = pq.top().second; + pq.pop(); + } + + return res; + } +}; + +// 用计数排序的方法解决这道题 +class Solution2 { +public: + vector topKFrequent(vector& nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + unordered_map valToFreq; + for (int v : nums) { + valToFreq[v]++; + } + + // 频率 -> 这个频率有哪些元素 + vector> freqToVals(nums.size() + 1); + for (auto entry : valToFreq) { + int val = entry.first; + int freq = entry.second; + freqToVals[freq].push_back(val); + } + + vector res; + // freqToVals 从后往前存储着出现最多的元素 + for (int i = freqToVals.size() - 1; i >= 0; i--) { + if (freqToVals[i].size() == 0) continue; + for (int j = 0; j < freqToVals[i].size(); j++) { + // 将出现次数最多的 k 个元素装入 res + res.push_back(freqToVals[i][j]); + if (res.size() == k) { + return res; + } + } + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +// 用优先级队列解决这道题 +func topKFrequent(nums []int, k int) []int { + // nums 中的元素 -> 该元素出现的频率 + valToFreq := make(map[int]int) + for _, v := range nums { + valToFreq[v] = valToFreq[v] + 1 + } + + // 个性化的 lambda + lessFn := func(a, b interface{}) bool { + a.(*MapEntry).Value.(int) + return a.(*MapEntry).Value.(int) < b.(*MapEntry).Value.(int) + } + + pq := priorityqueue.NewPriorityQueue(lessFn) + for key, val := range valToFreq { + pq.Insert(&MapEntry{Key: key, Value: val}) + if pq.Len() > k { + pq.Pop() + } + } + + res := make([]int, k) + for i := k - 1; i >= 0; i-- { + // res 数组中存储前 k 个最大元素 + res[i] = pq.Pop().(*MapEntry).Key.(int) + } + + return res +} + +// MapEntry 提供给优先级队列使用的数据结构 +type MapEntry struct { + Key interface{} + Value interface{} +} + +// 用计数排序的方法解决这道题 +func topKFrequent2(nums []int, k int) []int { + // nums 中的元素 -> 该元素出现的频率 + valToFreq := make(map[int]int) + for _, v := range nums { + valToFreq[v] = valToFreq[v] + 1 + } + + // 频率 -> 这个频率有哪些元素 + freqToVals := make([][]int, len(nums)+1) + for key, val := range valToFreq { + if freqToVals[val] == nil { + freqToVals[val] = make([]int, 0) + } + freqToVals[val] = append(freqToVals[val], key) + } + + res := make([]int, k) + p := 0 + // freqToVals 从后往前存储着出现最多的元素 + for i := len(freqToVals) - 1; i > 0; i-- { + valList := freqToVals[i] + if valList == nil { + continue + } + for _, val := range valList { + // 将出现次数最多的 k 个元素装入 res + res[p] = val + p++ + if p == k { + return res + } + } + } + + return nil +} +``` + +```java +// by labuladong (java) +// 用优先级队列解决这道题 +class Solution { + public int[] topKFrequent(int[] nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + HashMap valToFreq = new HashMap<>(); + for (int v : nums) { + valToFreq.put(v, valToFreq.getOrDefault(v, 0) + 1); + } + + PriorityQueue> + pq = new PriorityQueue<>((entry1, entry2) -> { + // 队列按照键值对中的值(元素出现频率)从小到大排序 + return entry1.getValue().compareTo(entry2.getValue()); + }); + + for (Map.Entry entry : valToFreq.entrySet()) { + pq.offer(entry); + if (pq.size() > k) { + // 弹出最小元素,维护队列内是 k 个频率最大的元素 + pq.poll(); + } + } + + int[] res = new int[k]; + for (int i = k - 1; i >= 0; i--) { + // res 数组中存储前 k 个最大元素 + res[i] = pq.poll().getKey(); + } + + return res; + } +} + +// 用计数排序的方法解决这道题 +class Solution2 { + public int[] topKFrequent(int[] nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + HashMap valToFreq = new HashMap<>(); + for (int v : nums) { + valToFreq.put(v, valToFreq.getOrDefault(v, 0) + 1); + } + + // 频率 -> 这个频率有哪些元素 + ArrayList[] freqToVals = new ArrayList[nums.length + 1]; + for (int val : valToFreq.keySet()) { + int freq = valToFreq.get(val); + if (freqToVals[freq] == null) { + freqToVals[freq] = new ArrayList<>(); + } + freqToVals[freq].add(val); + } + + int[] res = new int[k]; + int p = 0; + // freqToVals 从后往前存储着出现最多的元素 + for (int i = freqToVals.length - 1; i > 0; i--) { + ArrayList valList = freqToVals[i]; + if (valList == null) continue; + for (int j = 0; j < valList.size(); j++) { + // 将出现次数最多的 k 个元素装入 res + res[p] = valList.get(j); + p++; + if (p == k) { + return res; + } + } + } + + return null; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @param {number} k + * @return {number[]} + */ +var topKFrequent = function(nums, k) { + // nums 中的元素 -> 该元素出现的频率 + const valToFreq = new Map(); + for (const v of nums) { + valToFreq.set(v, (valToFreq.get(v) || 0) + 1); + } + + const compare = (entry1, entry2) => entry1[1] - entry2[1]; + // 队列按照键值对中的值(元素出现频率)从小到大排序 + const pq = new PriorityQueue(compare); + + for (const entry of valToFreq.entries()) { + pq.push(entry); + if (pq.size() > k) { + // 弹出最小元素,维护队列内是 k 个频率最大的元素 + pq.pop(); + } + } + + const res = new Array(k); + for (let i = k - 1; i >= 0; i--) { + // res 数组中存储前 k 个最大元素 + res[i] = pq.pop()[0]; + } + + return res; +}; + +/** + * @param {number[]} nums + * @param {number} k + * @return {number[]} + */ +var topKFrequent2 = function(nums, k) { + // nums 中的元素 -> 该元素出现的频率 + const valToFreq = new Map(); + for (const v of nums) { + valToFreq.set(v, (valToFreq.get(v) || 0) + 1); + } + + // 频率 -> 这个频率有哪些元素 + const maxFreq = nums.length; + const freqToVals = new Array(maxFreq + 1).map(() => new Array()); + for (const [val, freq] of valToFreq.entries()) { + freqToVals[freq].push(val); + } + + const res = new Array(k); + let p = 0; + // freqToVals 从后往前存储着出现最多的元素 + for (let freq = maxFreq; freq > 0; freq--) { + const valList = freqToVals[freq]; + for (const val of valList) { + // 将出现次数最多的 k 个元素装入 res + res[p] = val; + p++; + if (p === k) { + return res; + } + } + } + + return null; +} +``` + +```python +# by chatGPT (python) +class Solution: + def topKFrequent(self, nums: List[int], k: int) -> List[int]: + # nums 中的元素 -> 该元素出现的频率 + valToFreq = {} + for v in nums: + valToFreq[v] = valToFreq.get(v, 0) + 1 + + # 二叉堆按照键值对中的值(元素出现频率的负值)从小到大排列 + # 从二叉堆中pop出来的就是频率最大的键 (频率越大,负值越小) + pq = [(-freq, val) for val, freq in valToFreq.items()] + heapq.heapify(pq) + + # 将前 k 个最大元素装入 res + res = [] + for i in range(k): + res.append(heapq.heappop(pq)[1]) + + return res + + +class Solution2: + def topKFrequent(self, nums: List[int], k: int) -> List[int]: + # nums 中的元素 -> 该元素出现的频率 + valToFreq = {} + for v in nums: + valToFreq[v] = valToFreq.get(v, 0) + 1 + + # 频率 -> 这个频率有哪些元素 + freqToVals = [[] for _ in range(len(nums) + 1)] + for val, freq in valToFreq.items(): + freqToVals[freq].append(val) + + # freqToVals 从后往前存储着出现最多的元素 + res = [] + for i in range(len(nums), 0, -1): + if freqToVals[i]: + for val in freqToVals[i]: + res.append(val) + if len(res) == k: + return res + + return res +``` + +https://leetcode.cn/problems/g5c51o 的多语言解法👆 + +https://leetcode.cn/problems/gaM7Ch 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector memo; + + int coinChange(vector& coins, int amount) { + memo.resize(amount + 1, -666); + // dp 数组全都初始化为特殊值 + return dp(coins, amount); + } + + int dp(vector& coins, int amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; + + int res = INT_MAX; + for (int coin : coins) { + // 计算子问题的结果 + int subProblem = dp(coins, amount - coin); + /** + ![](../pictures/动态规划详解进阶/5.jpg) + */ + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = min(res, subProblem + 1); + } + // 把计算结果存入备忘录 + memo[amount] = (res == INT_MAX) ? -1 : res; + return memo[amount]; + } +}; +``` + +```go +// by chatGPT (go) +func coinChange(coins []int, amount int) int { + memo := make([]int, amount+1) + // dp 数组全都初始化为特殊值 + for i := 0; i < len(memo); i++ { + memo[i] = -666 + } + return dp(coins, amount, memo) +} + +func dp(coins []int, amount int, memo []int) int { + if amount == 0 { + return 0 + } + if amount < 0 { + return -1 + } + // 查备忘录,防止重复计算 + if memo[amount] != -666 { + return memo[amount] + } + + res := math.MaxInt32 + for _, coin := range coins { + // 计算子问题的结果 + subProblem := dp(coins, amount-coin, memo) + /** + ![](../pictures/动态规划详解进阶/5.jpg) + */ + // 子问题无解则跳过 + if subProblem == -1 { + continue + } + // 在子问题中选择最优解,然后加一 + res = min(res, subProblem+1) + } + // 把计算结果存入备忘录 + if res == math.MaxInt32 { + memo[amount] = -1 + } else { + memo[amount] = res + } + return memo[amount] +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + int[] memo; + + public int coinChange(int[] coins, int amount) { + memo = new int[amount + 1]; + // dp 数组全都初始化为特殊值 + Arrays.fill(memo, -666); + return dp(coins, amount); + } + + int dp(int[] coins, int amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; + + int res = Integer.MAX_VALUE; + for (int coin : coins) { + // 计算子问题的结果 + int subProblem = dp(coins, amount - coin); + /** + ![](../pictures/动态规划详解进阶/5.jpg) + */ + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); + } + // 把计算结果存入备忘录 + memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; + return memo[amount]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var coinChange = function(coins, amount) { + let memo = new Array(amount + 1).fill(-666); + return dp(coins, amount); + + function dp(coins, amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; + + let res = Number.MAX_VALUE; + for (let coin of coins) { + // 计算子问题的结果 + let subProblem = dp(coins, amount - coin); + /** + ![](../pictures/动态规划详解进阶/5.jpg) + */ + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); + } + // 把计算结果存入备忘录 + memo[amount] = (res == Number.MAX_VALUE) ? -1 : res; + return memo[amount]; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + memo = [-666] * (amount + 1) # dp 数组全都初始化为特殊值 + return self.dp(coins, amount, memo) + + def dp(self, coins: List[int], amount: int, memo: List[int]) -> int: + if amount == 0: + return 0 + if amount < 0: + return -1 + # 查备忘录,防止重复计算 + if memo[amount] != -666: + return memo[amount] + + res = float('inf') + for coin in coins: + # 计算子问题的结果 + sub_problem = self.dp(coins, amount - coin, memo) + # 子问题无解则跳过 + if sub_problem == -1: + continue + # 在子问题中选择最优解,然后加一 + res = min(res, sub_problem + 1) + + # 把计算结果存入备忘录 + memo[amount] = -1 if res == float('inf') else res + return memo[amount] +``` + +https://leetcode.cn/problems/gaM7Ch 的多语言解法👆 + +https://leetcode.cn/problems/gas-station 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int canCompleteCircuit(vector& gas, vector& cost) { + int n = gas.size(); + // 相当于图像中的坐标点和最低点 + int sum = 0, minSum = 0; + int start = 0; + for (int i = 0; i < n; i++) { + sum += gas[i] - cost[i]; + if (sum < minSum) { + // 经过第 i 个站点后,使 sum 到达新低 + // 所以站点 i + 1 就是最低点(起点) + start = i + 1; + minSum = sum; + } + } + if (sum < 0) { + // 总油量小于总的消耗,无解 + return -1; + } + // 环形数组特性 + return start == n ? 0 : start; + } +}; +``` + +```go +// by chatGPT (go) +func canCompleteCircuit(gas []int, cost []int) int { + n := len(gas) + // 相当于图像中的坐标点和最低点 + sum, minSum := 0, 0 + start := 0 + for i := 0; i < n; i++ { + sum += gas[i] - cost[i] + if sum < minSum { + // 经过第 i 个站点后,使 sum 到达新低 + // 所以站点 i + 1 就是最低点(起点) + start = i + 1 + minSum = sum + } + } + if sum < 0 { + // 总油量小于总的消耗,无解 + return -1 + } + // 环形数组特性 + if start == n { + return 0 + } + return start +} +``` + +```java +// by labuladong (java) +class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int n = gas.length; + // 相当于图像中的坐标点和最低点 + int sum = 0, minSum = 0; + int start = 0; + for (int i = 0; i < n; i++) { + sum += gas[i] - cost[i]; + if (sum < minSum) { + // 经过第 i 个站点后,使 sum 到达新低 + // 所以站点 i + 1 就是最低点(起点) + start = i + 1; + minSum = sum; + } + } + if (sum < 0) { + // 总油量小于总的消耗,无解 + return -1; + } + // 环形数组特性 + return start == n ? 0 : start; + } +} +``` + +```javascript +// by chatGPT (javascript) +var canCompleteCircuit = function(gas, cost) { + const n = gas.length; + // 相当于图像中的坐标点和最低点 + let sum = 0, minSum = 0; + let start = 0; + for (let i = 0; i < n; i++) { + sum += gas[i] - cost[i]; + if (sum < minSum) { + // 经过第 i 个站点后,使 sum 到达新低 + // 所以站点 i + 1 就是最低点(起点) + start = i + 1; + minSum = sum; + } + } + if (sum < 0) { + // 总油量小于总的消耗,无解 + return -1; + } + // 环形数组特性 + return start == n ? 0 : start; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: + n = len(gas) + # 相当于图像中的坐标点和最低点 + sum_, minSum, start = 0, 0, 0 + for i in range(n): + sum_ += gas[i] - cost[i] + if sum_ < minSum: + # 经过第 i 个站点后,使 sum 到达新低 + # 所以站点 i + 1 就是最低点(起点) + start = i + 1 + minSum = sum_ + if sum_ < 0: + # 总油量小于总的消耗,无解 + return -1 + # 环形数组特性 + return 0 if start == n else start +``` + +https://leetcode.cn/problems/gas-station 的多语言解法👆 + +https://leetcode.cn/problems/generate-parentheses 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector generateParenthesis(int n) { + if (n == 0) return {}; + // 记录所有合法的括号组合 + vector res; + // 回溯过程中的路径 + string track; + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, track, res); + return res; + } + + // 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个 + void backtrack(int left, int right, + string& track, vector& res) { + // 若左括号剩下的多,说明不合法 + if (right < left) return; + // 数量小于 0 肯定是不合法的 + if (left < 0 || right < 0) return; + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if (left == 0 && right == 0) { + res.push_back(track); + return; + } + + // 尝试放一个左括号 + track.push_back('('); // 选择 + backtrack(left - 1, right, track, res); + track.pop_back(); // 撤消选择 + + // 尝试放一个右括号 + track.push_back(')'); // 选择 + backtrack(left, right - 1, track, res); + track.pop_back(); // 撤消选择 + } +} +``` + +```go +// by chatGPT (go) +func generateParenthesis(n int) []string { + if n == 0 { + return []string{} + } + // 记录所有合法的括号组合 + res := []string{} + // 回溯过程中的路径 + var track string + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, &track, &res) + return res +} + +// 可用的左括号数量为 left 个,可用的右括号数量为 right 个 +func backtrack(left int, right int, track *string, res *[]string) { + // 若左括号剩下的多,说明不合法 + if right < left { + return + } + // 数量小于 0 肯定是不合法的 + if left < 0 || right < 0 { + return + } + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if left == 0 && right == 0 { + *res = append(*res, *track) + return + } + + // 尝试放一个左括号 + *track += "(" // 选择 + backtrack(left-1, right, track, res) + *track = (*track)[:len(*track)-1] // 撤消选择 + + // 尝试放一个右括号 + *track += ")" // 选择 + backtrack(left, right-1, track, res) + *track = (*track)[:len(*track)-1] // 撤消选择 +} +``` + +```java +// by chatGPT (java) +class Solution { + public List generateParenthesis(int n) { + if (n == 0) return new ArrayList<>(); + // 记录所有合法的括号组合 + List res = new ArrayList<>(); + // 回溯过程中的路径 + StringBuilder track = new StringBuilder(); + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, track, res); + return res; + } + + // 可用的左括号数量为 left 个,可用的右括号数量为 right 个 + void backtrack(int left, int right, + StringBuilder track, List res) { + // 若左括号剩下的多,说明不合法 + if (right < left) return; + // 数量小于 0 肯定是不合法的 + if (left < 0 || right < 0) return; + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if (left == 0 && right == 0) { + res.add(track.toString()); + return; + } + + // 尝试放一个左括号 + track.append('('); // 选择 + backtrack(left - 1, right, track, res); + track.deleteCharAt(track.length() - 1); // 撤消选择 + + // 尝试放一个右括号 + track.append(')'); // 选择 + backtrack(left, right - 1, track, res); + track.deleteCharAt(track.length() - 1); // 撤消选择 + } +} +``` + +```javascript +// by chatGPT (javascript) +var generateParenthesis = function(n) { + if (n === 0) return []; + // 记录所有合法的括号组合 + var res = []; + // 回溯过程中的路径 + var track = ""; + // 可用的左括号和右括号数量初始化为 n + backtrack(n, n, track, res); + return res; +}; + +// 可用的左括号数量为 left 个,可用的右括号数量为 right 个 +function backtrack(left, right, track, res) { + // 若左括号剩下的多,说明不合法 + if (right < left) return; + // 数量小于 0 肯定是不合法的 + if (left < 0 || right < 0) return; + // 当所有括号都恰好用完时,得到一个合法的括号组合 + if (left === 0 && right === 0) { + res.push(track); + return; + } + + // 尝试放一个左括号 + track += '('; // 选择 + backtrack(left - 1, right, track, res); + track = track.slice(0, -1); // 撤消选择 + + // 尝试放一个右括号 + track += ')'; // 选择 + backtrack(left, right - 1, track, res); + track = track.slice(0, -1); // 撤消选择 +} +``` + +```python +# by chatGPT (python) +class Solution: + def generateParenthesis(self, n: int) -> List[str]: + if n == 0: + return [] + # 记录所有合法的括号组合 + res = [] + # 回溯过程中的路径 + track = '' + # 可用的左括号和右括号数量初始化为 n + self.backtrack(n, n, track, res) + return res + + # 可用的左括号数量为 left 个,可用的右括号数量为 right 个 + def backtrack(self, left, right, track, res): + # 若左括号剩下的多,说明不合法 + if right < left: + return + # 数量小于 0 肯定是不合法的 + if left < 0 or right < 0: + return + # 当所有括号都恰好用完时,得到一个合法的括号组合 + if left == 0 and right == 0: + res.append(track) + return + + # 尝试放一个左括号 + track += '(' # 选择 + self.backtrack(left - 1, right, track, res) + track = track[:-1] # 撤消选择 + + # 尝试放一个右括号 + track += ')' # 选择 + self.backtrack(left, right - 1, track, res) + track = track[:-1] # 撤消选择 +``` + +https://leetcode.cn/problems/generate-parentheses 的多语言解法👆 + +https://leetcode.cn/problems/gou-jian-cheng-ji-shu-zu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector constructArr(vector& nums) { + int n = nums.size(); + if (n == 0) { + return vector(); + } + // 从左到右的前缀积,prefix[i] 是 nums[0..i] 的元素积 + vector prefix(n); + prefix[0] = nums[0]; + for (int i = 1; i < n; i++) { + prefix[i] = prefix[i - 1] * nums[i]; + } + // 从右到左的前缀积,suffix[i] 是 nums[i..n-1] 的元素积 + vector suffix(n); + suffix[n - 1] = nums[n - 1]; + for (int i = n - 2; i >= 0; i--) { + suffix[i] = suffix[i + 1] * nums[i]; + } + // 结果数组 + vector res(n); + res[0] = suffix[1]; + res[n - 1] = prefix[n - 2]; + for (int i = 1; i < n - 1; i++) { + // 除了 nums[i] 自己的元素积就是 nums[i] 左侧和右侧所有元素之积 + res[i] = prefix[i - 1] * suffix[i + 1]; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func constructArr(nums []int) []int { + n := len(nums) + if n == 0 { + return []int{} + } + // 从左到右的前缀积,prefix[i] 是 nums[0..i] 的元素积 + prefix := make([]int, n) + prefix[0] = nums[0] + for i := 1; i < n; i++ { + prefix[i] = prefix[i-1] * nums[i] + } + // 从右到左的前缀积,suffix[i] 是 nums[i..n-1] 的元素积 + suffix := make([]int, n) + suffix[n-1] = nums[n-1] + for i := n - 2; i >= 0; i-- { + suffix[i] = suffix[i+1] * nums[i] + } + // 结果数组 + res := make([]int, n) + res[0] = suffix[1] + res[n-1] = prefix[n-2] + for i := 1; i < n-1; i++ { + // 除了 nums[i] 自己的元素积就是 nums[i] 左侧和右侧所有元素之积 + res[i] = prefix[i-1] * suffix[i+1] + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] constructArr(int[] nums) { + int n = nums.length; + if (n == 0) { + return new int[0]; + } + // 从左到右的前缀积,prefix[i] 是 nums[0..i] 的元素积 + int[] prefix = new int[n]; + prefix[0] = nums[0]; + for (int i = 1; i < nums.length; i++) { + prefix[i] = prefix[i - 1] * nums[i]; + } + // 从右到左的前缀积,suffix[i] 是 nums[i..n-1] 的元素积 + int[] suffix = new int[n]; + suffix[n - 1] = nums[n - 1]; + for (int i = n - 2; i >= 0; i--) { + suffix[i] = suffix[i + 1] * nums[i]; + } + // 结果数组 + int[] res = new int[n]; + res[0] = suffix[1]; + res[n - 1] = prefix[n - 2]; + for (int i = 1; i < n - 1; i++) { + // 除了 nums[i] 自己的元素积就是 nums[i] 左侧和右侧所有元素之积 + res[i] = prefix[i - 1] * suffix[i + 1]; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var constructArr = function(nums) { + var n = nums.length; + if (n == 0) { + return new Array(0); + } + // 从左到右的前缀积,prefix[i] 是 nums[0..i] 的元素积 + var prefix = new Array(n); + prefix[0] = nums[0]; + for (var i = 1; i < nums.length; i++) { + prefix[i] = prefix[i - 1] * nums[i]; + } + // 从右到左的前缀积,suffix[i] 是 nums[i..n-1] 的元素积 + var suffix = new Array(n); + suffix[n - 1] = nums[n - 1]; + for (var i = n - 2; i >= 0; i--) { + suffix[i] = suffix[i + 1] * nums[i]; + } + // 结果数组 + var res = new Array(n); + res[0] = suffix[1]; + res[n - 1] = prefix[n - 2]; + for (var i = 1; i < n - 1; i++) { + // 除了 nums[i] 自己的元素积就是 nums[i] 左侧和右侧所有元素之积 + res[i] = prefix[i - 1] * suffix[i + 1]; + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def constructArr(self, nums: List[int]) -> List[int]: + n = len(nums) + if n == 0: + return [] + # 从左到右的前缀积,prefix[i] 是 nums[0..i] 的元素积 + prefix = [0] * n + prefix[0] = nums[0] + for i in range(1, n): + prefix[i] = prefix[i - 1] * nums[i] + # 从右到左的前缀积,suffix[i] 是 nums[i..n-1] 的元素积 + suffix = [0] * n + suffix[n - 1] = nums[n - 1] + for i in range(n - 2, -1, -1): + suffix[i] = suffix[i + 1] * nums[i] + # 结果数组 + res = [0] * n + res[0] = suffix[1] + res[n - 1] = prefix[n - 2] + for i in range(1, n - 1): + # 除了 nums[i] 自己的元素积就是 nums[i] 左侧和右侧所有元素之积 + res[i] = prefix[i - 1] * suffix[i + 1] + return res +``` + +https://leetcode.cn/problems/gou-jian-cheng-ji-shu-zu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/graph-valid-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool validTree(int n, vector>& edges) { + // 初始化 0...n-1 共 n 个节点 + UF uf(n); + // 遍历所有边,将组成边的两个节点进行连接 + for (auto edge : edges) { + int u = edge[0]; + int v = edge[1]; + // 若两个节点已经在同一连通分量中,会产生环 + if (uf.connected(u, v)) { + /** + ![](../pictures/kruskal/4.png) + */ + return false; + } + // 这条边不会产生环,可以是树的一部分 + uf.union(u, v); + /** + ![](../pictures/kruskal/5.png) + */ + } + // 要保证最后只形成了一棵树,即只有一个连通分量 + return uf.count() == 1; + } + +private: + class UF { + // 连通分量个数 + int count; + // 存储一棵树 + vector parent; + // 记录树的「重量」 + vector size; + + public: + // n 为图中节点的个数 + UF(int n) { + count = n; + parent.resize(n); + size.resize(n); + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + // 将节点 p 和节点 q 连通 + void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + bool connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + // 返回节点 x 的连通分量根节点 + int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + int count() { + return count; + } + }; +}; +``` + +```go +// by chatGPT (go) +type UF struct { + // 连通分量个数 + count int + parent []int + size []int +} + +func newUF(n int) *UF { + uf := new(UF) + uf.count = n + uf.parent = make([]int, n) + uf.size = make([]int, n) + for i := 0; i < n; i++ { + uf.parent[i] = i + uf.size[i] = 1 + } + return uf +} + +func (uf *UF) Union(p, q int) { + rootP := uf.Find(p) + rootQ := uf.Find(q) + if rootP == rootQ { + return + } + + // 小树接到大树下面,较平衡 + if uf.size[rootP] > uf.size[rootQ] { + uf.parent[rootQ] = rootP + uf.size[rootP] += uf.size[rootQ] + } else { + uf.parent[rootP] = rootQ + uf.size[rootQ] += uf.size[rootP] + } + // 两个连通分量合并成一个连通分量 + uf.count-- +} + +func (uf *UF) Connected(p, q int) bool { + rootP := uf.Find(p) + rootQ := uf.Find(q) + return rootP == rootQ +} + +func (uf *UF) Find(x int) int { + for uf.parent[x] != x { + // 进行路径压缩 + uf.parent[x] = uf.parent[uf.parent[x]] + x = uf.parent[x] + } + return x +} + +func (uf *UF) Count() int { + return uf.count +} + +func validTree(n int, edges [][]int) bool { + // 初始化 0...n-1 共 n 个节点 + uf := newUF(n) + // 遍历所有边,将组成边的两个节点进行连接 + for _, edge := range edges { + u, v := edge[0], edge[1] + // 若两个节点已经在同一连通分量中,会产生环 + if uf.Connected(u, v) { + /* + ![](../pictures/kruskal/4.png) + */ + return false + } + // 这条边不会产生环,可以是树的一部分 + uf.Union(u, v) + /* + ![](../pictures/kruskal/5.png) + */ + } + // 要保证最后只形成了一棵树,即只有一个连通分量 + return uf.Count() == 1 +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean validTree(int n, int[][] edges) { + // 初始化 0...n-1 共 n 个节点 + UF uf = new UF(n); + // 遍历所有边,将组成边的两个节点进行连接 + for (int[] edge : edges) { + int u = edge[0]; + int v = edge[1]; + // 若两个节点已经在同一连通分量中,会产生环 + if (uf.connected(u, v)) { + /** + ![](../pictures/kruskal/4.png) + */ + return false; + } + // 这条边不会产生环,可以是树的一部分 + uf.union(u, v); + /** + ![](../pictures/kruskal/5.png) + */ + } + // 要保证最后只形成了一棵树,即只有一个连通分量 + return uf.count() == 1; + } + + class UF { + // 连通分量个数 + private int count; + // 存储一棵树 + private int[] parent; + // 记录树的「重量」 + private int[] size; + + // n 为图中节点的个数 + public UF(int n) { + this.count = n; + parent = new int[n]; + size = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + // 将节点 p 和节点 q 连通 + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + // 返回节点 x 的连通分量根节点 + private int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + public int count() { + return count; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @param {number[][]} edges + * @return {boolean} + */ +var validTree = function(n, edges) { + // 初始化 0...n-1 共 n 个节点 + let uf = new UF(n); + // 遍历所有边,将组成边的两个节点进行连接 + for (let edge of edges) { + let u = edge[0]; + let v = edge[1]; + // 若两个节点已经在同一连通分量中,会产生环 + if (uf.connected(u, v)) { + return false; + } + // 这条边不会产生环,可以是树的一部分 + uf.union(u, v); + } + // 要保证最后只形成了一棵树,即只有一个连通分量 + return uf.count() == 1; +}; + +class UF { + constructor(n) { + this.count = n; + this.parent = new Array(n); + this.size = new Array(n); + for (let i = 0; i < n; i++) { + this.parent[i] = i; + this.size[i] = 1; + } + } + + // 将节点 p 和节点 q 连通 + union(p, q) { + let rootP = this.find(p); + let rootQ = this.find(q); + if (rootP === rootQ) { + return; + } + + // 小树接到大树下面,较平衡 + if (this.size[rootP] > this.size[rootQ]) { + this.parent[rootQ] = rootP; + this.size[rootP] += this.size[rootQ]; + } else { + this.parent[rootP] = rootQ; + this.size[rootQ] += this.size[rootP]; + } + // 两个连通分量合并成一个连通分量 + this.count--; + } + + // 判断节点 p 和节点 q 是否连通 + connected(p, q) { + let rootP = this.find(p); + let rootQ = this.find(q); + return rootP === rootQ; + } + + // 返回节点 x 的连通分量根节点 + find(x) { + while (this.parent[x] !== x) { + // 进行路径压缩 + this.parent[x] = this.parent[this.parent[x]]; + x = this.parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + count() { + return this.count; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def validTree(self, n: int, edges: List[List[int]]) -> bool: + # 初始化 0...n-1 共 n 个节点 + uf = UF(n) + # 遍历所有边,将组成边的两个节点进行连接 + for edge in edges: + u, v = edge[0], edge[1] + # 若两个节点已经在同一连通分量中,会产生环 + if uf.connected(u, v): + ''' + ![](../pictures/kruskal/4.png) + ''' + return False + # 这条边不会产生环,可以是树的一部分 + uf.union(u, v) + ''' + ![](../pictures/kruskal/5.png) + ''' + # 要保证最后只形成了一棵树,即只有一个连通分量 + return uf.count() == 1 + +class UF: + # 连通分量个数 + def count(self)->int: + return self.__count + + # n 为图中节点的个数 + def __init__(self, n: int): + self.__count = n + self.__parent = list(range(n)) + self.__size = [1] * n + + # 将节点 p 和节点 q 连通 + def union(self, p: int, q: int): + rootP = self.find(p) + rootQ = self.find(q) + if rootP == rootQ: + return + + # 小树接到大树下面,较平衡 + if self.__size[rootP] > self.__size[rootQ]: + self.__parent[rootQ] = rootP + self.__size[rootP] += self.__size[rootQ] + else: + self.__parent[rootP] = rootQ + self.__size[rootQ] += self.__size[rootP] + # 两个连通分量合并成一个连通分量 + self.__count -= 1 + + # 判断节点 p 和节点 q 是否连通 + def connected(self, p: int, q: int) -> bool: + rootP = self.find(p) + rootQ = self.find(q) + return rootP == rootQ + + # 返回节点 x 的连通分量根节点 + def find(self, x: int) -> int: + while self.__parent[x] != x: + # 进行路径压缩 + self.__parent[x] = self.__parent[self.__parent[x]] + x = self.__parent[x] + return x +``` + +https://leetcode.cn/problems/graph-valid-tree 的多语言解法👆 + +https://leetcode.cn/problems/group-anagrams 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> groupAnagrams(vector& strs) { + // 编码到分组的映射 + unordered_map> codeToGroup; + for (string s : strs) { + // 对字符串进行编码 + string code = encode(s); + // 把编码相同的字符串放在一起 + codeToGroup[code].push_back(s); + } + + // 获取结果 + vector> res; + for (auto group : codeToGroup) { + res.push_back(group.second); + } + + return res; + } + + // 利用每个字符的出现次数进行编码 + string encode(string s) { + vector count(26, 0); + for (char c : s) { + int delta = c - 'a'; + count[delta]++; + } + string code(count.begin(), count.end()); + return code; + } +}; +``` + +```go +// by chatGPT (go) +func groupAnagrams(strs []string) [][]string { + // 编码到分组的映射 + codeToGroup := make(map[string][]string) + for _, s := range strs { + // 对字符串进行编码 + code := encode(s) + // 把编码相同的字符串放在一起 + codeToGroup[code] = append(codeToGroup[code], s) + } + + // 获取结果 + res := make([][]string, 0, len(codeToGroup)) + for _, group := range codeToGroup { + res = append(res, group) + } + + return res +} + +// 利用每个字符的出现次数进行编码 +func encode(s string) string { + count := make([]byte, 26) + for i := 0; i < len(s); i++ { + delta := s[i] - 'a' + count[delta]++ + } + return string(count) +} +``` + +```java +// by labuladong (java) +class Solution { + public List> groupAnagrams(String[] strs) { + // 编码到分组的映射 + HashMap> codeToGroup = new HashMap<>(); + for (String s : strs) { + // 对字符串进行编码 + String code = encode(s); + // 把编码相同的字符串放在一起 + codeToGroup.putIfAbsent(code, new LinkedList<>()); + codeToGroup.get(code).add(s); + } + + // 获取结果 + List> res = new LinkedList<>(); + for (List group : codeToGroup.values()) { + res.add(group); + } + + return res; + } + + // 利用每个字符的出现次数进行编码 + String encode(String s) { + char[] count = new char[26]; + for (char c : s.toCharArray()) { + int delta = c - 'a'; + count[delta]++; + } + return new String(count); + } +} +``` + +```javascript +// by chatGPT (javascript) +var groupAnagrams = function(strs) { + // 编码到分组的映射 + let codeToGroup = new Map(); + for (let s of strs) { + // 对字符串进行编码 + let code = encode(s); + // 把编码相同的字符串放在一起 + if (!codeToGroup.has(code)) { + codeToGroup.set(code, []); + } + codeToGroup.get(code).push(s); + } + + // 获取结果 + let res = []; + for (let group of codeToGroup.values()) { + res.push(group); + } + + return res; +}; + +// 利用每个字符的出现次数进行编码 +function encode(s) { + let count = new Array(26).fill(0); + for (let c of s) { + let delta = c.charCodeAt() - 'a'.charCodeAt(); + count[delta]++; + } + return count.toString(); +} +``` + +```python +# by chatGPT (python) +class Solution: + def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + # 编码到分组的映射 + codeToGroup = {} + for s in strs: + # 对字符串进行编码 + code = self.encode(s) + # 把编码相同的字符串放在一起 + if code not in codeToGroup: + codeToGroup[code] = [] + codeToGroup[code].append(s) + + # 获取结果 + res = [] + for group in codeToGroup.values(): + res.append(group) + + return res + + # 利用每个字符的出现次数进行编码 + def encode(self, s: str) -> str: + count = [0] * 26 + for c in s: + delta = ord(c) - ord('a') + count[delta] += 1 + return str(count) +``` + +https://leetcode.cn/problems/group-anagrams 的多语言解法👆 + +https://leetcode.cn/problems/gu-piao-de-zui-da-li-run-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxProfit(vector& prices) { + int n = prices.size(); + vector> dp(n, vector(2)); + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = max(dp[i - 1][1], -prices[i]); + } + return dp[n - 1][0]; + } +}; +``` + +```go +// by chatGPT (go) +func maxProfit(prices []int) int { + n := len(prices) + dp := make([][]int, n) + for i := range dp { + dp[i] = make([]int, 2) + } + for i := 0; i < n; i++ { + if i-1 == -1 { + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + } + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + dp[i][1] = max(dp[i-1][1], -prices[i]) + } + return dp[n-1][0] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxProfit(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + } + return dp[n - 1][0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxProfit = function(prices) { + const n = prices.length; + const dp = Array.from(Array(n), () => new Array(2).fill(0)); + for (let i = 0; i < n; i++) { + if (i - 1 === -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + } + return dp[n - 1][0]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxProfit(self, prices: List[int]) -> int: + n = len(prices) + dp = [[0]*2 for _ in range(n)] + for i in range(n): + if i - 1 == -1: + dp[i][0] = 0 + dp[i][1] = -prices[i] + continue + dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]) + dp[i][1] = max(dp[i-1][1], -prices[i]) + return dp[n-1][0] +``` + +https://leetcode.cn/problems/gu-piao-de-zui-da-li-run-lcof 的多语言解法👆 + +https://leetcode.cn/problems/h54YBf 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Codec { + string SEP = ","; + string NULL = "#"; + +public: + /* 主函数,将二叉树序列化为字符串 */ + string serialize(TreeNode* root) { + stringstream ss; + serialize(root, ss); + return ss.str(); + } + + /* 辅助函数,将二叉树存入 stringstream */ + void serialize(TreeNode* root, stringstream& ss) { + if (root == nullptr) { + ss << NULL << SEP; + return; + } + + /******前序遍历位置******/ + ss << root->val << SEP; + /***********************/ + + serialize(root->left, ss); + serialize(root->right, ss); + } + + /* 主函数,将字符串反序列化为二叉树结构 */ + TreeNode* deserialize(string data) { + // 将字符串转化成列表 + vector nodes; + stringstream ss(data); + string s; + while (getline(ss, s, SEP[0])) { + nodes.push_back(s); + } + return deserialize(nodes); + } + + /* 辅助函数,通过 nodes 列表构造二叉树 */ + TreeNode* deserialize(vector& nodes) { + if (nodes.empty()) return nullptr; + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + string first = nodes[0]; + nodes.erase(nodes.begin()); + if (first == NULL) return nullptr; + TreeNode* root = new TreeNode(stoi(first)); + /***********************/ + + root->left = deserialize(nodes); + root->right = deserialize(nodes); + + return root; + } +}; +``` + +```go +// by chatGPT (go) +type Codec struct { + SEP string + NULL string +} + +func Constructor() Codec { + return Codec{",", "#"} +} + +/* 主函数,将二叉树序列化为字符串 */ +func (this *Codec) serialize(root *TreeNode) string { + var sb strings.Builder + this.serializeHelper(root, &sb) + return sb.String() +} + +/* 辅助函数,将二叉树存入 StringBuilder */ +func (this *Codec) serializeHelper(root *TreeNode, sb *strings.Builder) { + if root == nil { + sb.WriteString(this.NULL) + sb.WriteString(this.SEP) + return + } + + /******前序遍历位置******/ + sb.WriteString(strconv.Itoa(root.Val)) + sb.WriteString(this.SEP) + /***********************/ + + this.serializeHelper(root.Left, sb) + this.serializeHelper(root.Right, sb) +} + +/* 主函数,将字符串反序列化为二叉树结构 */ +func (this *Codec) deserialize(data string) *TreeNode { + // 将字符串转化成列表 + nodes := strings.Split(data, this.SEP) + return this.deserializeHelper(&nodes) +} + +/* 辅助函数,通过 nodes 列表构造二叉树 */ +func (this *Codec) deserializeHelper(nodes *[]string) *TreeNode { + if len(*nodes) == 0 { + return nil + } + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + first := (*nodes)[0] + *nodes = (*nodes)[1:] + if first == this.NULL { + return nil + } + val, _ := strconv.Atoi(first) + root := &TreeNode{val, nil, nil} + /***********************/ + + root.Left = this.deserializeHelper(nodes) + root.Right = this.deserializeHelper(nodes) + + return root +} +``` + +```java +// by labuladong (java) +public class Codec { + String SEP = ","; + String NULL = "#"; + + /* 主函数,将二叉树序列化为字符串 */ + public String serialize(TreeNode root) { + StringBuilder sb = new StringBuilder(); + serialize(root, sb); + return sb.toString(); + } + + /* 辅助函数,将二叉树存入 StringBuilder */ + void serialize(TreeNode root, StringBuilder sb) { + if (root == null) { + sb.append(NULL).append(SEP); + return; + } + + /******前序遍历位置******/ + sb.append(root.val).append(SEP); + /***********************/ + + serialize(root.left, sb); + serialize(root.right, sb); + } + + /* 主函数,将字符串反序列化为二叉树结构 */ + public TreeNode deserialize(String data) { + // 将字符串转化成列表 + LinkedList nodes = new LinkedList<>(); + for (String s : data.split(SEP)) { + nodes.addLast(s); + } + return deserialize(nodes); + } + + /* 辅助函数,通过 nodes 列表构造二叉树 */ + TreeNode deserialize(LinkedList nodes) { + if (nodes.isEmpty()) return null; + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + String first = nodes.removeFirst(); + if (first.equals(NULL)) return null; + TreeNode root = new TreeNode(Integer.parseInt(first)); + /***********************/ + + root.left = deserialize(nodes); + root.right = deserialize(nodes); + + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +var Codec = function() { + this.SEP = ","; + this.NULL = "#"; +}; + +/* 主函数,将二叉树序列化为字符串 */ +Codec.prototype.serialize = function(root) { + let sb = []; + this._serialize(root, sb); + return sb.join(''); +}; + +/* 辅助函数,将二叉树存入 StringBuilder */ +Codec.prototype._serialize = function(root, sb) { + if (!root) { + sb.push(this.NULL, this.SEP); + return; + } + + /******前序遍历位置******/ + sb.push(root.val, this.SEP); + /***********************/ + + this._serialize(root.left, sb); + this._serialize(root.right, sb); +}; + +/* 主函数,将字符串反序列化为二叉树结构 */ +Codec.prototype.deserialize = function(data) { + // 将字符串转化成列表 + let nodes = data.split(this.SEP); + return this._deserialize(nodes); +}; + +/* 辅助函数,通过 nodes 列表构造二叉树 */ +Codec.prototype._deserialize = function(nodes) { + if (!nodes.length) return null; + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + let first = nodes.shift(); + if (first === this.NULL) return null; + let root = new TreeNode(parseInt(first)); + /***********************/ + + root.left = this._deserialize(nodes); + root.right = this._deserialize(nodes); + + return root; +}; +``` + +```python +# by chatGPT (python) +class Codec: + SEP = "," + NULL = "#" + + def serialize(self, root: TreeNode) -> str: + """ + Encodes a tree to a single string. + + :param root: TreeNode + :return: str + """ + sb = [] + self._serialize(root, sb) + return self.SEP.join(sb) + + def _serialize(self, root: TreeNode, sb: List[str]) -> None: + """ + Helper function to serialize the tree. + + :param root: TreeNode + :param sb: List[str] + """ + if root is None: + sb.append(self.NULL) + return + + # 前序遍历位置 + sb.append(str(root.val)) + # 递归遍历左子树 + self._serialize(root.left, sb) + # 递归遍历右子树 + self._serialize(root.right, sb) + + def deserialize(self, data: str) -> TreeNode: + """ + Decodes your encoded data to tree. + + :param data: str + :return: TreeNode + """ + nodes = data.split(self.SEP) + return self._deserialize(nodes) + + def _deserialize(self, nodes: List[str]) -> TreeNode: + """ + Helper function to deserialize the tree. + + :param nodes: List[str] + :return: TreeNode + """ + if not nodes: + return None + + # 前序遍历位置 + val = nodes.pop(0) + if val == self.NULL: + return None + + # 创建当前节点 + root = TreeNode(int(val)) + # 递归构建左子树 + root.left = self._deserialize(nodes) + # 递归构建右子树 + root.right = self._deserialize(nodes) + + return root +``` + +https://leetcode.cn/problems/h54YBf 的多语言解法👆 + +https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { + // 虚拟头结点 + ListNode* dummy = new ListNode(-1); + ListNode* p = dummy; + ListNode* p1 = l1; + ListNode* p2 = l2; + + while (p1 != nullptr && p2 != nullptr) { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if (p1->val > p2->val) { + p->next = p2; + p2 = p2->next; + } else { + p->next = p1; + p1 = p1->next; + } + // p 指针不断前进 + p = p->next; + } + + if (p1 != nullptr) { + p->next = p1; + } + + if (p2 != nullptr) { + p->next = p2; + } + + return dummy->next; + } +}; +``` + +```go +// by chatGPT (go) +func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode { + // 虚拟头结点 + dummy := &ListNode{-1, nil} + p := dummy + p1, p2 := l1, l2 + + for p1 != nil && p2 != nil { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if p1.Val > p2.Val { + p.Next = p2 + p2 = p2.Next + } else { + p.Next = p1 + p1 = p1.Next + } + // p 指针不断前进 + p = p.Next + } + + if p1 != nil { + p.Next = p1 + } + + if p2 != nil { + p.Next = p2 + } + + return dummy.Next +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + // 虚拟头结点 + ListNode dummy = new ListNode(-1), p = dummy; + ListNode p1 = l1, p2 = l2; + + while (p1 != null && p2 != null) { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if (p1.val > p2.val) { + p.next = p2; + p2 = p2.next; + } else { + p.next = p1; + p1 = p1.next; + } + // p 指针不断前进 + p = p.next; + } + + if (p1 != null) { + p.next = p1; + } + + if (p2 != null) { + p.next = p2; + } + + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +var mergeTwoLists = function(l1, l2) { + // 虚拟头结点 + let dummy = new ListNode(-1), p = dummy; + let p1 = l1, p2 = l2; + + while (p1 != null && p2 != null) { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if (p1.val > p2.val) { + p.next = p2; + p2 = p2.next; + } else { + p.next = p1; + p1 = p1.next; + } + // p 指针不断前进 + p = p.next; + } + + if (p1 != null) { + p.next = p1; + } + + if (p2 != null) { + p.next = p2; + } + + return dummy.next; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: + # 虚拟头结点 + dummy = ListNode(-1) + p = dummy + p1, p2 = l1, l2 + + while p1 and p2: + # 比较 p1 和 p2 两个指针 + # 将值较小的的节点接到 p 指针 + if p1.val > p2.val: + p.next = p2 + p2 = p2.next + else: + p.next = p1 + p1 = p1.next + # p 指针不断前进 + p = p.next + + if p1: + p.next = p1 + if p2: + p.next = p2 + + return dummy.next +``` + +https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof 的多语言解法👆 + +https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector twoSum(vector& nums, int target) { + // 左右双指针 + int left = 0, right = nums.size() - 1; + while (left < right) { + int sum = nums[left] + nums[right]; + if (sum < target) { + // 让和大一点 + left++; + } else if (sum > target) { + // 让和小一点 + right--; + } else { + // 找到两个数 + return {nums[left], nums[right]}; + } + } + return {}; + } +}; +``` + +```go +// by chatGPT (go) +func twoSum(nums []int, target int) []int { + // 左右双指针 + left, right := 0, len(nums)-1 + for left < right { + sum := nums[left] + nums[right] + if sum < target { + // 让和大一点 + left++ + } else if sum > target { + // 让和小一点 + right-- + } else { + // 找到两个数 + return []int{nums[left], nums[right]} + } + } + return nil +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] twoSum(int[] nums, int target) { + // 左右双指针 + int left = 0, right = nums.length - 1; + while (left < right) { + int sum = nums[left] + nums[right]; + if (sum < target) { + // 让和大一点 + left++; + } else if (sum > target) { + // 让和小一点 + right--; + } else { + // 找到两个数 + return new int[]{nums[left], nums[right]}; + } + } + return null; + } +} +``` + +```javascript +// by chatGPT (javascript) +var twoSum = function(nums, target) { + // 左右双指针 + let left = 0, right = nums.length - 1; + while (left < right) { + let sum = nums[left] + nums[right]; + if (sum < target) { + // 让和大一点 + left++; + } else if (sum > target) { + // 让和小一点 + right--; + } else { + // 找到两个数 + return [nums[left], nums[right]]; + } + } + return null; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def twoSum(self, nums: List[int], target: int) -> List[int]: + # 左右双指针 + left, right = 0, len(nums) - 1 + while left < right: + sum = nums[left] + nums[right] + if sum < target: + # 让和大一点 + left += 1 + elif sum > target: + # 让和小一点 + right -= 1 + else: + # 找到两个数 + return [nums[left], nums[right]] + return None +``` + +https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/house-robber 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + // 备忘录 + vector memo; +public: + // 主函数 + int rob(vector& nums) { + // 初始化备忘录 + memo = vector(nums.size(), -1); + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0); + } + + // 返回 dp[start..] 能抢到的最大值 + int dp(vector& nums, int start) { + if (start >= nums.size()) { + return 0; + } + // 避免重复计算 + if (memo[start] != -1) { + return memo[start]; + } + + int res = max(dp(nums, start + 1), nums[start] + dp(nums, start + 2)); + // 记入备忘录 + memo[start] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +import "math" + +// 主函数 +func rob(nums []int) int { + // 备忘录数组 + memo := make([]int, len(nums)) + // 填充备忘录数组为 -1 + for i := range memo { + memo[i] = -1 + } + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0, memo) +} + +// 返回 dp[start..] 能抢到的最大值 +func dp(nums []int, start int, memo []int) int { + if start >= len(nums) { + return 0 + } + // 避免重复计算 + if memo[start] != -1 { + return memo[start] + } + + res := int(math.Max(float64(dp(nums, start+1, memo)), float64(nums[start]+dp(nums, start+2, memo)))) + // 记入备忘录 + memo[start] = res + return res +} +``` + +```java +// by labuladong (java) +class Solution { + // 备忘录 + private int[] memo; + // 主函数 + public int rob(int[] nums) { + // 初始化备忘录 + memo = new int[nums.length]; + Arrays.fill(memo, -1); + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0); + } + + // 返回 dp[start..] 能抢到的最大值 + private int dp(int[] nums, int start) { + if (start >= nums.length) { + return 0; + } + // 避免重复计算 + if (memo[start] != -1) return memo[start]; + + int res = Math.max(dp(nums, start + 1), + nums[start] + dp(nums, start + 2)); + // 记入备忘录 + memo[start] = res; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var rob = function(nums) { + // 备忘录 + let memo = new Array(nums.length).fill(-1); + // 强盗从第 0 间房子开始抢劫 + return dp(nums, 0, memo); +}; + +// 返回 dp[start..] 能抢到的最大值 +function dp(nums, start, memo) { + if (start >= nums.length) { + return 0; + } + // 避免重复计算 + if (memo[start] != -1) return memo[start]; + + let res = Math.max(dp(nums, start + 1, memo), + nums[start] + dp(nums, start + 2, memo)); + // 记入备忘录 + memo[start] = res; + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.memo = [] + + def rob(self, nums: List[int]) -> int: + self.memo = [-1] * len(nums) + # 强盗从第 0 间房子开始抢劫 + return self.dp(nums, 0) + + # 返回 dp[start..] 能抢到的最大值 + def dp(self, nums: List[int], start: int) -> int: + if start >= len(nums): + return 0 + # 避免重复计算 + if self.memo[start] != -1: + return self.memo[start] + + res = max(self.dp(nums, start + 1), + nums[start] + self.dp(nums, start + 2)) + # 记入备忘录 + self.memo[start] = res + return res +``` + +https://leetcode.cn/problems/house-robber 的多语言解法👆 + +https://leetcode.cn/problems/house-robber-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int rob(vector& nums) { + int n = nums.size(); + if (n == 1) return nums[0]; + + vector memo1(n, -1); + vector memo2(n, -1); + // 两次调用使用两个不同的备忘录 + return max( + dp(nums, 0, n - 2, memo1), + dp(nums, 1, n - 1, memo2) + ); + } + + // 定义:计算闭区间 [start,end] 的最优结果 + int dp(vector& nums, int start, int end, vector& memo) { + if (start > end) { + return 0; + } + + if (memo[start] != -1) { + return memo[start]; + } + // 状态转移方程 + int res = max( + dp(nums, start + 2, end, memo) + nums[start], + dp(nums, start + 1, end, memo) + ); + + memo[start] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +func rob(nums []int) int { + n := len(nums) + if n == 1 { + return nums[0] + } + + memo1 := make([]int, n) + memo2 := make([]int, n) + for i := 0; i < n; i++ { + memo1[i] = -1 + memo2[i] = -1 + } + // 两次调用使用两个不同的备忘录 + return max( + func() int { + res := dp(nums, 0, n-2, memo1) + return res + }(), + func() int { + res := dp(nums, 1, n-1, memo2) + return res + }(), + ) +} + +// 定义:计算闭区间 [start,end] 的最优结果 +func dp(nums []int, start, end int, memo []int) int { + if start > end { + return 0 + } + + if memo[start] != -1 { + return memo[start] + } + // 状态转移方程 + res := max( + func() int { + res := dp(nums, start+2, end, memo) + nums[start] + return res + }(), + func() int { + res := dp(nums, start+1, end, memo) + return res + }(), + ) + + memo[start] = res + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + + public int rob(int[] nums) { + int n = nums.length; + if (n == 1) return nums[0]; + + int[] memo1 = new int[n]; + int[] memo2 = new int[n]; + Arrays.fill(memo1, -1); + Arrays.fill(memo2, -1); + // 两次调用使用两个不同的备忘录 + return Math.max( + dp(nums, 0, n - 2, memo1), + dp(nums, 1, n - 1, memo2) + ); + } + + // 定义:计算闭区间 [start,end] 的最优结果 + int dp(int[] nums, int start, int end, int[] memo) { + if (start > end) { + return 0; + } + + if (memo[start] != -1) { + return memo[start]; + } + // 状态转移方程 + int res = Math.max( + dp(nums, start + 2, end, memo) + nums[start], + dp(nums, start + 1, end, memo) + ); + + memo[start] = res; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var rob = function(nums) { + const n = nums.length; + if (n == 1) return nums[0]; + + const memo1 = new Array(n).fill(-1); + const memo2 = new Array(n).fill(-1); + // 两次调用使用两个不同的备忘录 + return Math.max( + dp(nums, 0, n - 2, memo1), + dp(nums, 1, n - 1, memo2) + ); +}; + +// 定义:计算闭区间 [start,end] 的最优结果 +const dp = function(nums, start, end, memo) { + if (start > end) { + return 0; + } + + if (memo[start] != -1) { + return memo[start]; + } + // 状态转移方程 + const res = Math.max( + dp(nums, start + 2, end, memo) + nums[start], + dp(nums, start + 1, end, memo) + ); + + memo[start] = res; + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def rob(self, nums: List[int]) -> int: + n = len(nums) + if n == 1: + return nums[0] + + memo1 = [-1] * n + memo2 = [-1] * n + # 两次调用使用两个不同的备忘录 + return max( + self.dp(nums, 0, n - 2, memo1), + self.dp(nums, 1, n - 1, memo2) + ) + + # 定义:计算闭区间 [start,end] 的最优结果 + def dp(self, nums: List[int], start: int, end: int, memo: List[int]) -> int: + if start > end: + return 0 + + if memo[start] != -1: + return memo[start] + # 状态转移方程 + res = max( + self.dp(nums, start + 2, end, memo) + nums[start], + self.dp(nums, start + 1, end, memo) + ) + + memo[start] = res + return res +``` + +https://leetcode.cn/problems/house-robber-ii 的多语言解法👆 + +https://leetcode.cn/problems/house-robber-iii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + unordered_map memo; + + int rob(TreeNode* root) { + if (root == nullptr) return 0; + // 利用备忘录消除重叠子问题 + if (memo.count(root)) + return memo[root]; + // 抢,然后去下下家 + int do_it = root->val + + (root->left == nullptr ? + 0 : rob(root->left->left) + rob(root->left->right)) + + (root->right == nullptr ? + 0 : rob(root->right->left) + rob(root->right->right)); + // 不抢,然后去下家 + int not_do = rob(root->left) + rob(root->right); + + int res = max(do_it, not_do); + memo[root] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +// Definition for a binary tree node. +// type TreeNode struct { +// Val int +// Left *TreeNode +// Right *TreeNode +// } + +func rob(root *TreeNode) int { + memo := make(map[*TreeNode]int) + var robSub func(*TreeNode) int + robSub = func(node *TreeNode) int { + if node == nil { + return 0 + } + if _, ok := memo[node]; ok { + return memo[node] + } + //考虑偷取该节点 + doIt := node.Val + if node.Left != nil { + doIt += robSub(node.Left.Left) + robSub(node.Left.Right) + } + if node.Right != nil { + doIt += robSub(node.Right.Left) + robSub(node.Right.Right) + } + //不偷取该节点 + notDo := robSub(node.Left) + robSub(node.Right) + res := max(doIt, notDo) + memo[node] = res + return res + } + return robSub(root) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + Map memo = new HashMap<>(); + + public int rob(TreeNode root) { + if (root == null) return 0; + // 利用备忘录消除重叠子问题 + if (memo.containsKey(root)) + return memo.get(root); + // 抢,然后去下下家 + int do_it = root.val + + (root.left == null ? + 0 : rob(root.left.left) + rob(root.left.right)) + + (root.right == null ? + 0 : rob(root.right.left) + rob(root.right.right)); + // 不抢,然后去下家 + int not_do = rob(root.left) + rob(root.right); + + int res = Math.max(do_it, not_do); + memo.put(root, res); + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var rob = function(root) { + const memo = new Map(); + if (root === null) { + return 0; + } + // 利用备忘录消除重叠子问题 + if (memo.has(root)) { + return memo.get(root); + } + // 抢,然后去下下家 + let do_it = root.val + + ((root.left === null) ? + 0 : + rob(root.left.left) + rob(root.left.right)) + + ((root.right === null) ? + 0 : + rob(root.right.left) + rob(root.right.right)); + // 不抢,然后去下家 + let not_do = rob(root.left) + rob(root.right); + let res = Math.max(do_it, not_do); + memo.set(root, res); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.memo = {} + + def rob(self, root: TreeNode) -> int: + if not root: + return 0 + + # 利用备忘录消除重叠子问题 + if root in self.memo: + return self.memo[root] + + # 抢,然后去下下家 + do_it = root.val + if root.left: + do_it += self.rob(root.left.left) + self.rob(root.left.right) + if root.right: + do_it += self.rob(root.right.left) + self.rob(root.right.right) + + # 不抢,然后去下家 + not_do = self.rob(root.left) + self.rob(root.right) + + res = max(do_it, not_do) + self.memo[root] = res + return res +``` + +https://leetcode.cn/problems/house-robber-iii 的多语言解法👆 + +https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + /* 单调队列的实现 */ + class MonotonicQueue { + deque q; + public: + void push(int n) { + // 将小于 n 的元素全部删除 + while (!q.empty() && q.back() < n) { + /** + ![](../pictures/单调队列/3.png) + */ + q.pop_back(); + } + // 然后将 n 加入尾部 + q.push_back(n); + } + + int max() { + return q.front(); + } + + void pop(int n) { + if (n == q.front()) { + q.pop_front(); + } + } + }; + +public: + /* 解题函数的实现 */ + vector maxSlidingWindow(vector& nums, int k) { + MonotonicQueue window; + vector res; + + for (int i = 0; i < nums.size(); i++) { + if (i < k - 1) { + //先填满窗口的前 k - 1 + window.push(nums[i]); + } else { + /** + ![](../pictures/单调队列/1.png) + */ + // 窗口向前滑动,加入新数字 + window.push(nums[i]); + // 记录当前窗口的最大值 + res.push_back(window.max()); + // 移出旧数字 + window.pop(nums[i - k + 1]); + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +type MonotonicQueue struct { + q []int +} + +func (mq *MonotonicQueue) push(n int) { + // 将小于 n 的元素全部删除 + for len(mq.q) > 0 && mq.q[len(mq.q)-1] < n { + mq.q = mq.q[:len(mq.q)-1] + } + // 然后将 n 加入尾部 + mq.q = append(mq.q, n) +} + +func (mq *MonotonicQueue) max() int { + return mq.q[0] +} + +func (mq *MonotonicQueue) pop(n int) { + if n == mq.q[0] { + mq.q = mq.q[1:] + } +} + +func maxSlidingWindow(nums []int, k int) []int { + window := &MonotonicQueue{} + res := []int{} + + for i := 0; i < len(nums); i++ { + if i < k-1 { + // 先填满窗口的前 k - 1 + window.push(nums[i]) + } else { + // 窗口向前滑动,加入新数字 + window.push(nums[i]) + // 记录当前窗口的最大值 + res = append(res, window.max()) + // 移出旧数字 + window.pop(nums[i-k+1]) + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + /* 单调队列的实现 */ + class MonotonicQueue { + LinkedList q = new LinkedList<>(); + public void push(int n) { + // 将小于 n 的元素全部删除 + while (!q.isEmpty() && q.getLast() < n) { + /** + ![](../pictures/单调队列/3.png) + */ + q.pollLast(); + } + // 然后将 n 加入尾部 + q.addLast(n); + } + + public int max() { + return q.getFirst(); + } + + public void pop(int n) { + if (n == q.getFirst()) { + q.pollFirst(); + } + } + } + + /* 解题函数的实现 */ + public int[] maxSlidingWindow(int[] nums, int k) { + MonotonicQueue window = new MonotonicQueue(); + List res = new ArrayList<>(); + + for (int i = 0; i < nums.length; i++) { + if (i < k - 1) { + //先填满窗口的前 k - 1 + window.push(nums[i]); + } else { + /** + ![](../pictures/单调队列/1.png) + */ + // 窗口向前滑动,加入新数字 + window.push(nums[i]); + // 记录当前窗口的最大值 + res.add(window.max()); + // 移出旧数字 + window.pop(nums[i - k + 1]); + } + } + // 需要转成 int[] 数组再返回 + int[] arr = new int[res.size()]; + for (int i = 0; i < res.size(); i++) { + arr[i] = res.get(i); + } + return arr; + } +} +``` + +```javascript +// by chatGPT (javascript) +var Solution = function() { + /* 单调队列的实现 */ + class MonotonicQueue { + constructor() { + this.q = []; + } + push(n) { + // 将小于 n 的元素全部删除 + while (this.q.length > 0 && this.q[this.q.length - 1] < n) { + /** + ![](../pictures/单调队列/3.png) + */ + this.q.pop(); + } + // 然后将 n 加入尾部 + this.q.push(n); + } + max() { + return this.q[0]; + } + pop(n) { + if (n === this.q[0]) { + this.q.shift(); + } + } + } + + /* 解题函数的实现 */ + this.maxSlidingWindow = function(nums, k) { + const window = new MonotonicQueue(); + const res = []; + + for (let i = 0; i < nums.length; i++) { + if (i < k - 1) { + // 先填满窗口的前 k - 1 + window.push(nums[i]); + } else { + /** + ![](../pictures/单调队列/1.png) + */ + // 窗口向前滑动,加入新数字 + window.push(nums[i]); + // 记录当前窗口的最大值 + res.push(window.max()); + // 移出旧数字 + window.pop(nums[i - k + 1]); + } + } + // 需要转成 int[] 数组再返回 + return res; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + # 单调队列的实现 + class MonotonicQueue: + def __init__(self): + self.q = [] + def push(self, n: int) -> None: + # 将小于 n 的元素全部删除 + while self.q and self.q[-1] < n: + ''' + + ![](../pictures/单调队列/3.png) + ''' + self.q.pop() + # 然后将 n 加入尾部 + self.q.append(n) + + def max(self) -> int: + return self.q[0] + + def pop(self, n: int) -> None: + if n == self.q[0]: + self.q.pop(0) + + # 解题函数的实现 + def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: + window = self.MonotonicQueue() + res = [] + + for i in range(len(nums)): + if i < k - 1: + # 先填满窗口的前 k - 1 + window.push(nums[i]) + else: + ''' + + ![](../pictures/单调队列/1.png) + ''' + # 窗口向前滑动,加入新数字 + window.push(nums[i]) + # 记录当前窗口的最大值 + res.append(window.max()) + # 移出旧数字 + window.pop(nums[i - k + 1]) + + # 需要转成 int[] 数组再返回 + return res +``` + +https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/implement-queue-using-stacks 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class MyQueue { +private: + stack s1, s2; + +public: + MyQueue() {} + + /** + * 添加元素到队尾 + */ + void push(int x) { + s1.push(x); + } + + /** + * 删除队头的元素并返回 + */ + int pop() { + // 先调用 peek 保证 s2 非空 + peek(); + int element = s2.top(); + s2.pop(); + return element; + } + + /** + * 返回队头元素 + */ + int peek() { + if (s2.empty()) + // 把 s1 元素压入 s2 + while (!s1.empty()) { + s2.push(s1.top()); + s1.pop(); + } + return s2.top(); + } + + /** + * 判断队列是否为空 + */ + bool empty() { + return s1.empty() && s2.empty(); + } +}; +``` + +```go +// by chatGPT (go) +type MyQueue struct { + s1, s2 []int +} + +func Constructor() MyQueue { + return MyQueue{} +} + +/** + * 添加元素到队尾 + */ +func (q *MyQueue) Push(x int) { + q.s1 = append(q.s1, x) +} + +/** + * 删除队头的元素并返回 + */ +func (q *MyQueue) Pop() int { + // 先调用 Peek 保证 s2 非空 + q.Peek() + val := q.s2[len(q.s2)-1] + q.s2 = q.s2[:len(q.s2)-1] + return val +} + +/** + * 返回队头元素 + */ +func (q *MyQueue) Peek() int { + if len(q.s2) == 0 { + // 把 s1 元素压入 s2 + for len(q.s1) != 0 { + q.s2 = append(q.s2, q.s1[len(q.s1)-1]) + q.s1 = q.s1[:len(q.s1)-1] + } + } + return q.s2[len(q.s2)-1] +} + +/** + * 判断队列是否为空 + */ +func (q *MyQueue) Empty() bool { + return len(q.s1) == 0 && len(q.s2) == 0 +} +``` + +```java +// by labuladong (java) +class MyQueue { + private Stack s1, s2; + + public MyQueue() { + s1 = new Stack<>(); + s2 = new Stack<>(); + } + + /** + * 添加元素到队尾 + */ + public void push(int x) { + s1.push(x); + } + + /** + * 删除队头的元素并返回 + */ + public int pop() { + // 先调用 peek 保证 s2 非空 + peek(); + return s2.pop(); + } + + /** + * 返回队头元素 + */ + public int peek() { + if (s2.isEmpty()) + // 把 s1 元素压入 s2 + while (!s1.isEmpty()) + s2.push(s1.pop()); + return s2.peek(); + } + + /** + * 判断队列是否为空 + */ + public boolean empty() { + return s1.isEmpty() && s2.isEmpty(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var MyQueue = function() { + this.s1 = []; + this.s2 = []; +}; + +/** + * 添加元素到队尾 + */ +MyQueue.prototype.push = function(x) { + this.s1.push(x); +}; + +/** + * 删除队头的元素并返回 + */ +MyQueue.prototype.pop = function() { + // 先调用 peek 保证 s2 非空 + this.peek(); + return this.s2.pop(); +}; + +/** + * 返回队头元素 + */ +MyQueue.prototype.peek = function() { + if (this.s2.length === 0) + // 把 s1 元素压入 s2 + while (this.s1.length > 0) + this.s2.push(this.s1.pop()); + return this.s2[this.s2.length - 1]; +}; + +/** + * 判断队列是否为空 + */ +MyQueue.prototype.empty = function() { + return this.s1.length === 0 && this.s2.length === 0; +}; +``` + +```python +# by chatGPT (python) +class MyQueue: + + def __init__(self): + self.s1 = [] + self.s2 = [] + + def push(self, x: int) -> None: + self.s1.append(x) + + def pop(self) -> int: + # 先调用 peek 保证 s2 非空 + self.peek() + return self.s2.pop() + + def peek(self) -> int: + if not self.s2: + # 把 s1 元素压入 s2 + while self.s1: + self.s2.append(self.s1.pop()) + return self.s2[-1] + + def empty(self) -> bool: + return not self.s1 and not self.s2 +``` + +https://leetcode.cn/problems/implement-queue-using-stacks 的多语言解法👆 + +https://leetcode.cn/problems/implement-stack-using-queues 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class MyStack { + queue q; + int top_elem = 0; + +public: + /** + * 添加元素到栈顶 + */ + void push(int x) { + // x 是队列的队尾,是栈的栈顶 + q.push(x); + top_elem = x; + } + + /** + * 返回栈顶元素 + */ + int top() { + return top_elem; + } + + /** + * 删除栈顶的元素并返回 + */ + int pop() { + int size = q.size(); + // 留下队尾 2 个元素 + while (size > 2) { + q.push(q.front()); + q.pop(); + size--; + } + // 记录新的队尾元素 + top_elem = q.front(); + q.push(q.front()); + q.pop(); + // 删除之前的队尾元素 + int tmp=q.front(); + q.pop(); + return tmp; + } + + /** + * 判断栈是否为空 + */ + bool empty() { + return q.empty(); + } +}; +``` + +```go +// by mario-huang (go) +package ImplementStackusingQueues + +type MyStack struct { + q []int + topElem int +} + +func Constructor() MyStack { + return MyStack{q: []int{}, topElem: 0} +} + +/** + * 添加元素到栈顶 + */ +func (this *MyStack) Push(x int) { + // x 是队列的队尾,是栈的栈顶 + this.q = append(this.q, x) + this.topElem = x +} + +/** + * 删除栈顶的元素并返回 + */ +func (this *MyStack) Pop() int { + size := len(this.q) + // 留下队尾 2 个元素 + for size > 2 { + this.q = append(this.q, this.q[0]) + this.q = this.q[1:] + size-- + } + // 记录新的队尾元素 + this.topElem = this.q[0] + this.q = append(this.q, this.q[0]) + this.q = this.q[1:] + // 删除之前的队尾元素 + val := this.q[0] + this.q = this.q[1:] + return val +} + +/** + * 返回栈顶元素 + */ +func (this *MyStack) Top() int { + return this.topElem +} + +/** + * 判断栈是否为空 + */ +func (this *MyStack) Empty() bool { + return len(this.q) == 0 +} +``` + +```java +// by labuladong (java) +class MyStack { + Queue q = new LinkedList<>(); + int top_elem = 0; + + /** + * 添加元素到栈顶 + */ + public void push(int x) { + // x 是队列的队尾,是栈的栈顶 + q.offer(x); + top_elem = x; + } + + /** + * 返回栈顶元素 + */ + public int top() { + return top_elem; + } + + /** + * 删除栈顶的元素并返回 + */ + public int pop() { + int size = q.size(); + // 留下队尾 2 个元素 + while (size > 2) { + q.offer(q.poll()); + size--; + } + // 记录新的队尾元素 + top_elem = q.peek(); + q.offer(q.poll()); + // 删除之前的队尾元素 + return q.poll(); + } + + /** + * 判断栈是否为空 + */ + public boolean empty() { + return q.isEmpty(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var MyStack = function() { + this.q = []; + this.top_elem = 0; +}; + +/** + * 添加元素到栈顶 + */ +MyStack.prototype.push = function(x) { + // x 是队列的队尾,是栈的栈顶 + this.q.push(x); + this.top_elem = x; +}; + +/** + * 返回栈顶元素 + */ +MyStack.prototype.top = function() { + return this.top_elem; +}; + +/** + * 删除栈顶的元素并返回 + */ +MyStack.prototype.pop = function() { + var size = this.q.length; + // 留下队尾 2 个元素 + while (size > 2) { + this.q.push(this.q.shift()); + size--; + } + // 记录新的队尾元素 + this.top_elem = this.q[0]; + this.q.push(this.q.shift()); + // 删除之前的队尾元素 + return this.q.shift(); +}; + +/** + * 判断栈是否为空 + */ +MyStack.prototype.empty = function() { + return this.q.length === 0; +}; +``` + +```python +# by chatGPT (python) +from queue import Queue + +class MyStack: + def __init__(self): + self.q = Queue() + self.top_elem = 0 + + def push(self, x: int) -> None: + """ + 添加元素到栈顶 + """ + # x 是队列的队尾,是栈的栈顶 + self.q.put(x) + self.top_elem = x + + def pop(self) -> int: + """ + 删除栈顶的元素并返回 + """ + size = self.q.qsize() + # 留下队尾 2 个元素 + while size > 2: + self.q.put(self.q.get()) + size -= 1 + # 记录新的队尾元素 + self.top_elem = self.q.queue[0] + self.q.put(self.q.get()) + # 删除之前的队尾元素 + return self.q.get() + + def top(self) -> int: + """ + 返回栈顶元素 + """ + return self.top_elem + + def empty(self) -> bool: + """ + 判断栈是否为空 + """ + return self.q.empty() +``` + +https://leetcode.cn/problems/implement-stack-using-queues 的多语言解法👆 + +https://leetcode.cn/problems/insert-delete-getrandom-o1 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class RandomizedSet { + public: + // 存储元素的值 + vector nums; + // 记录每个元素对应在 nums 中的索引 + unordered_map valToIndex; + + bool insert(int val) { + // 若 val 已存在,不用再插入 + if (valToIndex.count(val)) { + return false; + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + valToIndex[val] = nums.size(); + nums.push_back(val); + return true; + } + + bool remove(int val) { + // 若 val 不存在,不用再删除 + if (!valToIndex.count(val)) { + return false; + } + // 先拿到 val 的索引 + int index = valToIndex[val]; + // 将最后一个元素对应的索引修改为 index + valToIndex[nums.back()] = index; + // 交换 val 和最后一个元素 + swap(nums[index], nums.back()); + // 在数组中删除元素 val + nums.pop_back(); + // 删除元素 val 对应的索引 + valToIndex.erase(val); + return true; + } + + int getRandom() { + // 随机获取 nums 中的一个元素 + return nums[rand() % nums.size()]; + } +}; +``` + +```go +// by chatGPT (go) +type RandomizedSet struct { + // 存储元素的值 + nums []int + // 记录每个元素对应在 nums 中的索引 + valToIndex map[int]int +} + +func Constructor() RandomizedSet { + return RandomizedSet{ + nums: []int{}, + valToIndex: make(map[int]int), + } +} + +func (this *RandomizedSet) Insert(val int) bool { + // 若 val 已存在,不用再插入 + if _, ok := this.valToIndex[val]; ok { + return false + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + this.valToIndex[val] = len(this.nums) + this.nums = append(this.nums, val) + return true +} + +func (this *RandomizedSet) Remove(val int) bool { + // 若 val 不存在,不用再删除 + if _, ok := this.valToIndex[val]; !ok { + return false + } + // 先拿到 val 的索引 + index := this.valToIndex[val] + // 将最后一个元素对应的索引修改为 index + this.valToIndex[this.nums[len(this.nums)-1]] = index + // 交换 val 和最后一个元素 + this.nums[index], this.nums[len(this.nums)-1] = this.nums[len(this.nums)-1], this.nums[index] + // 在数组中删除元素 val + this.nums = this.nums[:len(this.nums)-1] + // 删除元素 val 对应的索引 + delete(this.valToIndex, val) + return true +} + +func (this *RandomizedSet) GetRandom() int { + // 随机获取 nums 中的一个元素 + return this.nums[rand.Intn(len(this.nums))] +} +``` + +```java +// by chatGPT (java) +class RandomizedSet { + // 存储元素的值 + List nums; + // 记录每个元素对应在 nums 中的索引 + Map valToIndex; + + public RandomizedSet() { + nums = new ArrayList<>(); + valToIndex = new HashMap<>(); + } + + public boolean insert(int val) { + // 若 val 已存在,不用再插入 + if (valToIndex.containsKey(val)) { + return false; + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + valToIndex.put(val, nums.size()); + nums.add(val); + return true; + } + + public boolean remove(int val) { + // 若 val 不存在,不用再删除 + if (!valToIndex.containsKey(val)) { + return false; + } + // 先拿到 val 的索引 + int index = valToIndex.get(val); + // 将最后一个元素对应的索引修改为 index + valToIndex.put(nums.get(nums.size() - 1), index); + // 交换 val 和最后一个元素 + Collections.swap(nums, index, nums.size() - 1); + // 在数组中删除元素 val + nums.remove(nums.size() - 1); + // 删除元素 val 对应的索引 + valToIndex.remove(val); + return true; + } + + public int getRandom() { + // 随机获取 nums 中的一个元素 + return nums.get((int)(Math.random() * nums.size())); + } +} +``` + +```javascript +// by chatGPT (javascript) +var RandomizedSet = function() { + // 存储元素的值 + this.nums = []; + // 记录每个元素对应在 nums 中的索引 + this.valToIndex = {}; +}; + +RandomizedSet.prototype.insert = function(val) { + // 若 val 已存在,不用再插入 + if (this.valToIndex[val] !== undefined) { + return false; + } + // 若 val 不存在,插入到 nums 尾部, + // 并记录 val 对应的索引值 + this.valToIndex[val] = this.nums.length; + this.nums.push(val); + return true; +}; + +RandomizedSet.prototype.remove = function(val) { + // 若 val 不存在,不用再删除 + if (this.valToIndex[val] === undefined) { + return false; + } + // 先拿到 val 的索引 + var index = this.valToIndex[val]; + // 将最后一个元素对应的索引修改为 index + this.valToIndex[this.nums[this.nums.length - 1]] = index; + // 交换 val 和最后一个元素 + var temp = this.nums[index]; + this.nums[index] = this.nums[this.nums.length - 1]; + this.nums[this.nums.length - 1] = temp; + // 在数组中删除元素 val + this.nums.pop(); + // 删除元素 val 对应的索引 + delete this.valToIndex[val]; + return true; +}; + +RandomizedSet.prototype.getRandom = function() { + // 随机获取 nums 中的一个元素 + return this.nums[Math.floor(Math.random() * this.nums.length)]; +}; +``` + +```python +# by chatGPT (python) +class RandomizedSet: + + def __init__(self): + """ + Initialize your data structure here. + """ + self.nums = [] # 存储元素的值 + self.valToIndex = dict() # 记录每个元素对应在 nums 中的索引 + + def insert(self, val: int) -> bool: + """ + Inserts a value to the set. Returns true if the set did not already contain the specified element. + """ + # 若 val 已存在,不用再插入 + if val in self.valToIndex: + return False + # 若 val 不存在,插入到 nums 尾部, + # 并记录 val 对应的索引值 + self.valToIndex[val] = len(self.nums) + self.nums.append(val) + return True + + def remove(self, val: int) -> bool: + """ + Removes a value from the set. Returns true if the set contained the specified element. + """ + # 若 val 不存在,不用再删除 + if val not in self.valToIndex: + return False + # 先拿到 val 的索引 + index = self.valToIndex[val] + # 将最后一个元素对应的索引修改为 index + self.valToIndex[self.nums[-1]] = index + # 交换 val 和最后一个元素 + self.nums[index], self.nums[-1] = self.nums[-1], self.nums[index] + # 在数组中删除元素 val + self.nums.pop() + # 删除元素 val 对应的索引 + del self.valToIndex[val] + return True + + def getRandom(self) -> int: + """ + Get a random element from the set. + """ + # 随机获取 nums 中的一个元素 + return self.nums[random.randint(0, len(self.nums) - 1)] +``` + +https://leetcode.cn/problems/insert-delete-getrandom-o1 的多语言解法👆 + +https://leetcode.cn/problems/insert-into-a-binary-search-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + TreeNode* insertIntoBST(TreeNode* root, int val) { + // 找到空位置插入新节点 + if (root == nullptr) return new TreeNode(val); + // if (root->val == val) + // BST 中一般不会插入已存在元素 + if (root->val < val) + root->right = insertIntoBST(root->right, val); + if (root->val > val) + root->left = insertIntoBST(root->left, val); + return root; + } +}; +``` + +```go +// by chatGPT (go) +func insertIntoBST(root *TreeNode, val int) *TreeNode { + // 找到空位置插入新节点 + if root == nil { + return &TreeNode{Val: val} + } + // if (root.val == val) + // BST 中一般不会插入已存在元素 + if root.Val < val { + root.Right = insertIntoBST(root.Right, val) + } + if root.Val > val { + root.Left = insertIntoBST(root.Left, val) + } + return root +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode insertIntoBST(TreeNode root, int val) { + // 找到空位置插入新节点 + if (root == null) return new TreeNode(val); + // if (root.val == val) + // BST 中一般不会插入已存在元素 + if (root.val < val) + root.right = insertIntoBST(root.right, val); + if (root.val > val) + root.left = insertIntoBST(root.left, val); + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +var insertIntoBST = function(root, val) { + // 找到空位置插入新节点 + if (root == null) return new TreeNode(val); + // if (root.val == val) + // BST 中一般不会插入已存在元素 + if (root.val < val) + root.right = insertIntoBST(root.right, val); + if (root.val > val) + root.left = insertIntoBST(root.left, val); + return root; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode: + # 找到空位置插入新节点 + if root is None: + return TreeNode(val) + # if (root.val == val) + # BST 中一般不会插入已存在元素 + if root.val < val: + root.right = self.insertIntoBST(root.right, val) + if root.val > val: + root.left = self.insertIntoBST(root.left, val) + return root +``` + +https://leetcode.cn/problems/insert-into-a-binary-search-tree 的多语言解法👆 + +https://leetcode.cn/problems/intersection-of-two-linked-lists 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + ListNode *p1 = headA, *p2 = headB; + while (p1 != p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 == nullptr) p1 = headB; + else p1 = p1->next; + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 == nullptr) p2 = headA; + else p2 = p2->next; + } + return p1; + } +}; +``` + +```go +// by chatGPT (go) +func getIntersectionNode(headA, headB *ListNode) *ListNode { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + p1, p2 := headA, headB + for p1 != p2 { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if p1 == nil { + p1 = headB + } else { + p1 = p1.Next + } + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if p2 == nil { + p2 = headA + } else { + p2 = p2.Next + } + } + return p1 +} +``` + +```java +// by labuladong (java) +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + ListNode p1 = headA, p2 = headB; + while (p1 != p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 == null) p1 = headB; + else p1 = p1.next; + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 == null) p2 = headA; + else p2 = p2.next; + } + return p1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var getIntersectionNode = function(headA, headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + let p1 = headA, p2 = headB; + while (p1 !== p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 === null) p1 = headB; + else p1 = p1.next; + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 === null) p2 = headA; + else p2 = p2.next; + } + return p1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: + # p1 指向 A 链表头结点,p2 指向 B 链表头结点 + p1, p2 = headA, headB + while p1 != p2: + # p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if p1 is None: + p1 = headB + else: + p1 = p1.next + # p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if p2 is None: + p2 = headA + else: + p2 = p2.next + return p1 +``` + +https://leetcode.cn/problems/intersection-of-two-linked-lists 的多语言解法👆 + +https://leetcode.cn/problems/interval-list-intersections 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> intervalIntersection(vector>& A, vector>& B) { + vector> res; + int i = 0, j = 0; + while (i < A.size() && j < B.size()) { + int a1 = A[i][0], a2 = A[i][1]; + int b1 = B[j][0], b2 = B[j][1]; + + if (b2 >= a1 && a2 >= b1) { + res.push_back({ + max(a1, b1), min(a2, b2) + }); + } + if (b2 < a2) { + j++; + } else { + i++; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func intervalIntersection(A [][]int, B [][]int) [][]int { + res := [][]int{} + i, j := 0, 0 + for i < len(A) && j < len(B) { + a1, a2 := A[i][0], A[i][1] + b1, b2 := B[j][0], B[j][1] + + if b2 >= a1 && a2 >= b1 { + res = append(res, []int{ + max(a1, b1), min(a2, b2), + }) + } + if b2 < a2 { + j++ + } else { + i++ + } + } + return res +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] intervalIntersection(int[][] A, int[][] B) { + List res = new LinkedList<>(); + int i = 0, j = 0; + while (i < A.length && j < B.length) { + int a1 = A[i][0], a2 = A[i][1]; + int b1 = B[j][0], b2 = B[j][1]; + + if (b2 >= a1 && a2 >= b1) { + res.add(new int[]{ + Math.max(a1, b1), Math.min(a2, b2) + }); + } + if (b2 < a2) { + j++; + } else { + i++; + } + } + return res.toArray(new int[0][0]); + } +} +``` + +```javascript +// by chatGPT (javascript) +var intervalIntersection = function(A, B) { + const res = []; + let i = 0, j = 0; + while (i < A.length && j < B.length) { + const a1 = A[i][0], a2 = A[i][1]; + const b1 = B[j][0], b2 = B[j][1]; + + if (b2 >= a1 && a2 >= b1) { + res.push([ + Math.max(a1, b1), Math.min(a2, b2) + ]); + } + if (b2 < a2) { + j++; + } else { + i++; + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def intervalIntersection(self, A: List[List[int]], B: List[List[int]]) -> List[List[int]]: + res = [] + i, j = 0, 0 + while i < len(A) and j < len(B): + a1, a2 = A[i][0], A[i][1] + b1, b2 = B[j][0], B[j][1] + + if b2 >= a1 and a2 >= b1: + res.append([max(a1, b1), min(a2, b2)]) + + if b2 < a2: + j += 1 + else: + i += 1 + + return res +``` + +https://leetcode.cn/problems/interval-list-intersections 的多语言解法👆 + +https://leetcode.cn/problems/invert-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 「遍历」的思路 +class Solution { +public: + // 主函数 + TreeNode* invertTree(TreeNode* root) { + // 遍历二叉树,交换每个节点的子节点 + traverse(root); + return root; + } + + // 二叉树遍历函数 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + + /**** 前序位置 ****/ + // 每一个节点需要做的事就是交换它的左右子节点 + TreeNode* tmp = root->left; + root->left = root->right; + root->right = tmp; + + // 遍历框架,去遍历左右子树的节点 + traverse(root->left); + traverse(root->right); + } +}; + +// 「分解问题」的思路 +class Solution2 { +public: + // 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + TreeNode* invertTree(TreeNode* root) { + if (root == nullptr) { + return nullptr; + } + // 利用函数定义,先翻转左右子树 + TreeNode* left = invertTree(root->left); + TreeNode* right = invertTree(root->right); + + // 然后交换左右子节点 + root->left = right; + root->right = left; + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +//「遍历」的思路 +func invertTree(root *TreeNode) *TreeNode { + //遍历二叉树,交换每个节点的子节点 + traverse(root) + return root +} + +//二叉树遍历函数 +func traverse(root *TreeNode) { + if root == nil { + return + } + // 每一个节点需要做的事就是交换它的左右子节点 + tmp := root.Left + root.Left = root.Right + root.Right = tmp + // 遍历框架,去遍历左右子树的节点 + traverse(root.Left) + traverse(root.Right) +} + +// 「分解问题」的思路 +func invertTree2(root *TreeNode) *TreeNode { + if root == nil { + return nil + } + // 利用函数定义,先翻转左右子树 + left := invertTree2(root.Left) + right := invertTree2(root.Right) + // 然后交换左右子节点 + root.Left = right + root.Right = left + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root +} +``` + +```java +// by labuladong (java) +// 「遍历」的思路 +class Solution { + // 主函数 + public TreeNode invertTree(TreeNode root) { + // 遍历二叉树,交换每个节点的子节点 + traverse(root); + return root; + } + + // 二叉树遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + + /**** 前序位置 ****/ + // 每一个节点需要做的事就是交换它的左右子节点 + TreeNode tmp = root.left; + root.left = root.right; + root.right = tmp; + + // 遍历框架,去遍历左右子树的节点 + traverse(root.left); + traverse(root.right); + } +} + +// 「分解问题」的思路 +class Solution2 { + // 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + TreeNode invertTree(TreeNode root) { + if (root == null) { + return null; + } + // 利用函数定义,先翻转左右子树 + TreeNode left = invertTree(root.left); + TreeNode right = invertTree(root.right); + + // 然后交换左右子节点 + root.left = right; + root.right = left; + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 「遍历」的思路 + */ +var invertTree = function(root) { + /** + * 二叉树遍历函数 + */ + var traverse = function(root) { + if (root === null) { + return; + } + + /**** 前序位置 ****/ + // 每一个节点需要做的事就是交换它的左右子节点 + var tmp = root.left; + root.left = root.right; + root.right = tmp; + + // 遍历框架,去遍历左右子树的节点 + traverse(root.left); + traverse(root.right); + } + + // 遍历二叉树,交换每个节点的子节点 + traverse(root); + return root; +}; + +/** + * 「分解问题」的思路 + */ +var invertTree2 = function(root) { + /** + * 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + */ + var invert = function(root) { + if (root === null) { + return null; + } + // 利用函数定义,先翻转左右子树 + var left = invert(root.left); + var right = invert(root.right); + + // 然后交换左右子节点 + root.left = right; + root.right = left; + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root; + } + + // 返回翻转后的二叉树的根节点 + return invert(root); +}; +``` + +```python +# by chatGPT (python) +# 「遍历」的思路 +class Solution: + # 主函数 + def invertTree(self, root: TreeNode) -> TreeNode: + # 遍历二叉树,交换每个节点的子节点 + self.traverse(root) + return root + + # 二叉树遍历函数 + def traverse(self, root: TreeNode) -> None: + if not root: + return + + # 前序位置 + # 每一个节点需要做的事就是交换它的左右子节点 + tmp = root.left + root.left = root.right + root.right = tmp + + # 遍历框架,去遍历左右子树的节点 + self.traverse(root.left) + self.traverse(root.right) + + +# 「分解问题」的思路 +class Solution2: + # 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + def invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + # 利用函数定义,先翻转左右子树 + left = self.invertTree(root.left) + right = self.invertTree(root.right) + + # 然后交换左右子节点 + root.left = right + root.right = left + + # 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root +``` + +https://leetcode.cn/problems/invert-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/is-graph-bipartite 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 记录图是否符合二分图性质 + bool ok = true; + // 记录图中节点的颜色,false 和 true 代表两种不同颜色 + vector color; + // 记录图中节点是否被访问过 + vector visited; + + // 主函数,输入邻接表,判断是否是二分图 + bool isBipartite(vector>& graph) { + int n = graph.size(); + color.resize(n); + visited.resize(n); + // 因为图不一定是联通的,可能存在多个子图 + // 所以要把每个节点都作为起点进行一次遍历 + // 如果发现任何一个子图不是二分图,整幅图都不算二分图 + for (int v = 0; v < n; v++) { + if (!visited[v]) { + traverse(graph, v); + } + } + return ok; + } + + // DFS 遍历框架 + void traverse(vector>& graph, int v) { + // 如果已经确定不是二分图了,就不用浪费时间再递归遍历了 + if (!ok) return; + + visited[v] = true; + for (int w : graph[v]) { + if (!visited[w]) { + // 相邻节点 w 没有被访问过 + // 那么应该给节点 w 涂上和节点 v 不同的颜色 + color[w] = !color[v]; + // 继续遍历 w + traverse(graph, w); + } else { + // 相邻节点 w 已经被访问过 + // 根据 v 和 w 的颜色判断是否是二分图 + if (color[w] == color[v]) { + // 若相同,则此图不是二分图 + ok = false; + } + } + } + } +}; +``` + +```go +// by chatGPT (go) +func isBipartite(graph [][]int) bool { + n := len(graph) + // 记录图是否符合二分图性质 + ok := true + // 记录图中节点的颜色,false 和 true 代表两种不同颜色 + color := make([]bool, n) + // 记录图中节点是否被访问过 + visited := make([]bool, n) + + traverse := func(v int) { + // 如果已经确定不是二分图了,就不用浪费时间再递归遍历了 + if !ok { + return + } + + visited[v] = true + for _, w := range graph[v] { + if !visited[w] { + // 相邻节点 w 没有被访问过 + // 那么应该给节点 w 涂上和节点 v 不同的颜色 + color[w] = !color[v] + // 继续遍历 w + traverse(w) + } else { + // 相邻节点 w 已经被访问过 + // 根据 v 和 w 的颜色判断是否是二分图 + if color[w] == color[v] { + // 若相同,则此图不是二分图 + ok = false + } + } + } + } + + // 因为图不一定是联通的,可能存在多个子图 + // 所以要把每个节点都作为起点进行一次遍历 + // 如果发现任何一个子图不是二分图,整幅图都不算二分图 + for v := 0; v < n; v++ { + if !visited[v] { + traverse(v) + } + } + return ok +} +``` + +```java +// by labuladong (java) +class Solution { + + // 记录图是否符合二分图性质 + private boolean ok = true; + // 记录图中节点的颜色,false 和 true 代表两种不同颜色 + private boolean[] color; + // 记录图中节点是否被访问过 + private boolean[] visited; + + // 主函数,输入邻接表,判断是否是二分图 + public boolean isBipartite(int[][] graph) { + int n = graph.length; + color = new boolean[n]; + visited = new boolean[n]; + // 因为图不一定是联通的,可能存在多个子图 + // 所以要把每个节点都作为起点进行一次遍历 + // 如果发现任何一个子图不是二分图,整幅图都不算二分图 + for (int v = 0; v < n; v++) { + if (!visited[v]) { + traverse(graph, v); + } + } + return ok; + } + + // DFS 遍历框架 + private void traverse(int[][] graph, int v) { + // 如果已经确定不是二分图了,就不用浪费时间再递归遍历了 + if (!ok) return; + + visited[v] = true; + for (int w : graph[v]) { + if (!visited[w]) { + // 相邻节点 w 没有被访问过 + // 那么应该给节点 w 涂上和节点 v 不同的颜色 + color[w] = !color[v]; + // 继续遍历 w + traverse(graph, w); + } else { + // 相邻节点 w 已经被访问过 + // 根据 v 和 w 的颜色判断是否是二分图 + if (color[w] == color[v]) { + // 若相同,则此图不是二分图 + ok = false; + } + } + } + } + +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} graph 邻接表 + * @return {boolean} 是否是二分图 + */ +var isBipartite = function(graph) { + // 记录图是否符合二分图性质 + let ok = true; + // 记录图中节点的颜色,false 和 true 代表两种不同颜色 + const color = new Array(graph.length).fill(false); + // 记录图中节点是否被访问过 + const visited = new Array(graph.length).fill(false); + + /** + * DFS 遍历框架 + * @param {number} v 节点 + */ + const traverse = function(v) { + // 如果已经确定不是二分图了,就不用浪费时间再递归遍历了 + if (!ok) { + return; + } + + visited[v] = true; + for (const w of graph[v]) { + if (!visited[w]) { + // 相邻节点 w 没有被访问过 + // 那么应该给节点 w 涂上和节点 v 不同的颜色 + color[w] = !color[v]; + // 继续遍历 w + traverse(w); + } else { + // 相邻节点 w 已经被访问过 + // 根据 v 和 w 的颜色判断是否是二分图 + if (color[w] === color[v]) { + // 若相同,则此图不是二分图 + ok = false; + } + } + } + } + + // 因为图不一定是联通的,可能存在多个子图 + // 所以要把每个节点都作为起点进行一次遍历 + // 如果发现任何一个子图不是二分图,整幅图都不算二分图 + for (let v = 0; v < graph.length; v++) { + if (!visited[v]) { + traverse(v); + } + } + + return ok; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + # 记录图是否符合二分图性质 + self.ok = True + # 记录图中节点的颜色,false 和 true 代表两种不同颜色 + self.color = None + # 记录图中节点是否被访问过 + self.visited = None + + # 主函数,输入邻接表,判断是否是二分图 + def isBipartite(self, graph: List[List[int]]) -> bool: + n = len(graph) + self.color = [False] * n + self.visited = [False] * n + # 因为图不一定是联通的,可能存在多个子图 + # 所以要把每个节点都作为起点进行一次遍历 + # 如果发现任何一个子图不是二分图,整幅图都不算二分图 + for v in range(n): + if not self.visited[v]: + self.traverse(graph, v) + return self.ok + + # DFS 遍历框架 + def traverse(self, graph: List[List[int]], v: int) -> None: + # 如果已经确定不是二分图了,就不用浪费时间再递归遍历了 + if not self.ok: + return + + self.visited[v] = True + for w in graph[v]: + if not self.visited[w]: + # 相邻节点 w 没有被访问过 + # 那么应该给节点 w 涂上和节点 v 不同的颜色 + self.color[w] = not self.color[v] + # 继续遍历 w + self.traverse(graph, w) + else: + # 相邻节点 w 已经被访问过 + # 根据 v 和 w 的颜色判断是否是二分图 + if self.color[w] == self.color[v]: + # 若相同,则此图不是二分图 + self.ok = False +``` + +https://leetcode.cn/problems/is-graph-bipartite 的多语言解法👆 + +https://leetcode.cn/problems/is-subsequence 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isSubsequence(string s, string t) { + int i = 0, j = 0; + while (i < s.length() && j < t.length()) { + if (s[i] == t[j]) { + i++; + } + j++; + } + return i == s.length(); + } +}; +``` + +```go +// by chatGPT (go) +func isSubsequence(s string, t string) bool { + i, j := 0, 0 + for i < len(s) && j < len(t) { + if s[i] == t[j] { + i++ + } + j++ + } + return i == len(s) +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isSubsequence(String s, String t) { + int i = 0, j = 0; + while (i < s.length() && j < t.length()) { + if (s.charAt(i) == t.charAt(j)) { + i++; + } + j++; + } + return i == s.length(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @param {string} t + * @return {boolean} + */ +var isSubsequence = function(s, t) { + let i = 0, j = 0; + while (i < s.length && j < t.length) { + if (s.charAt(i) === t.charAt(j)) { + i++; + } + j++; + } + return i === s.length; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isSubsequence(self, s: str, t: str) -> bool: + i, j = 0, 0 + while i < len(s) and j < len(t): + if s[i] == t[j]: + i += 1 + j += 1 + return i == len(s) +``` + +https://leetcode.cn/problems/is-subsequence 的多语言解法👆 + +https://leetcode.cn/problems/jBjn9C 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class KthLargest { +private: + int k; + // 默认是小顶堆 + priority_queue, greater> pq; +public: + KthLargest(int k, vector& nums) { + // 将 nums 装入小顶堆,保留下前 k 大的元素 + for (int e : nums) { + pq.push(e); + if (pq.size() > k) { + pq.pop(); + } + } + this->k = k; + } + + int add(int val) { + // 维护小顶堆只保留前 k 大的元素 + pq.push(val); + if (pq.size() > k) { + pq.pop(); + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return pq.top(); + } +}; +``` + +```go +// by chatGPT (go) +import ( + "container/heap" +) + +// KthLargest 是一个数据结构,它维护了一个保留前 k 大元素的小顶堆 +type KthLargest struct { + k int + pq PriorityQueue +} + +// Constructor 是 KthLargest 的构造函数 +func Constructor(k int, nums []int) KthLargest { + pq := make(PriorityQueue, 0, k) + for _, e := range nums { + pq.offer(e) + if pq.Len() > k { + pq.poll() + } + } + return KthLargest{ + k: k, + pq: pq, + } +} + +// add 方法将一个元素添加到小顶堆中,并返回第 k 大元素 +func (kl *KthLargest) add(val int) int { + kl.pq.offer(val) + if kl.pq.Len() > kl.k { + kl.pq.poll() + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return kl.pq.peek() +} + +// PriorityQueue 是一个带有 Peek 方法的小顶堆 +type PriorityQueue []int + +// Len 返回小顶堆中的元素数量 +func (pq PriorityQueue) Len() int { + return len(pq) +} + +// Less 定义了小顶堆的排序规则 +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i] < pq[j] +} + +// Swap 交换小顶堆中的两个元素的位置 +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +// Push 向小顶堆中添加一个元素 +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(int) + *pq = append(*pq, item) +} + +// Pop 从小顶堆中弹出最小的元素 +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[:n-1] + return item +} + +// Peek 返回小顶堆中的最小元素 +func (pq PriorityQueue) Peek() int { + return pq[0] +} +``` + +```java +// by labuladong (java) +class KthLargest { + + private int k; + // 默认是小顶堆 + private PriorityQueue pq = new PriorityQueue<>(); + + public KthLargest(int k, int[] nums) { + // 将 nums 装入小顶堆,保留下前 k 大的元素 + for (int e : nums) { + pq.offer(e); + if (pq.size() > k) { + pq.poll(); + } + } + this.k = k; + } + + public int add(int val) { + // 维护小顶堆只保留前 k 大的元素 + pq.offer(val); + if (pq.size() > k) { + pq.poll(); + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return pq.peek(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var KthLargest = function(k, nums) { + this.k = k; + // 默认是小顶堆 + this.pq = new PriorityQueue(); + // 将 nums 装入小顶堆,保留下前 k 大的元素 + for (let e of nums) { + this.pq.offer(e); + if (this.pq.size() > k) { + this.pq.poll(); + } + } +}; + +KthLargest.prototype.add = function(val) { + // 维护小顶堆只保留前 k 大的元素 + this.pq.offer(val); + if (this.pq.size() > this.k) { + this.pq.poll(); + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return this.pq.peek(); +}; + +// PriorityQueue implementation +var PriorityQueue = function() { + this.data = []; +}; + +PriorityQueue.prototype.offer = function(val) { + this.data.push(val); + this.bubbleUp(this.data.length - 1); +}; + +PriorityQueue.prototype.poll = function() { + const last = this.data.pop(); + const result = this.data[0]; + if (this.data.length > 0) { + this.data[0] = last; + this.bubbleDown(0); + } + return result; +}; + +PriorityQueue.prototype.peek = function() { + return this.data[0]; +}; + +PriorityQueue.prototype.size = function() { + return this.data.length; +}; + +PriorityQueue.prototype.bubbleUp = function(pos) { + while (pos > 0) { + const parent = Math.floor((pos - 1) / 2); + if (this.data[parent] > this.data[pos]) { + this.swap(parent, pos); + pos = parent; + } else { + break; + } + } +}; + +PriorityQueue.prototype.bubbleDown = function(pos) { + while (pos * 2 + 1 < this.data.length) { + let minChild = pos * 2 + 1; + if (pos * 2 + 2 < this.data.length && this.data[pos * 2 + 2] < this.data[minChild]) { + minChild = pos * 2 + 2; + } + if (this.data[minChild] < this.data[pos]) { + this.swap(minChild, pos); + pos = minChild; + } else { + break; + } + } +}; + +PriorityQueue.prototype.swap = function(i, j) { + const temp = this.data[i]; + this.data[i] = this.data[j]; + this.data[j] = temp; +}; +``` + +```python +# by chatGPT (python) +from queue import PriorityQueue + +class KthLargest: + def __init__(self, k: int, nums: List[int]): + self.k = k + # 默认是小顶堆 + self.pq = PriorityQueue() + # 将 nums 装入小顶堆,保留下前 k 大的元素 + for e in nums: + self.pq.put(e) + if self.pq.qsize() > k: + self.pq.get() + + def add(self, val: int) -> int: + # 维护小顶堆只保留前 k 大的元素 + self.pq.put(val) + if self.pq.qsize() > self.k: + self.pq.get() + # 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return self.pq.queue[0] +``` + +https://leetcode.cn/problems/jBjn9C 的多语言解法👆 + +https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int movingCount(int m, int n, int k) { + vector> visited(m, vector(n, false)); // 初始化 visited 矩阵为 false + dfs(m, n, k, 0, 0, visited); + return res; + } + + // 记录合法坐标数 + int res = 0; + + void dfs(int m, int n, int k, int i, int j, vector>& visited) { + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + + if (i / 10 + i % 10 + j / 10 + j % 10 > k) { + // 坐标和超出 k 的限制 + return; + } + + if (visited[i][j]) { + // 之前已经访问过当前坐标 + return; + } + + // 走到一个合法坐标 + res++; + visited[i][j] = true; + + // DFS 遍历上下左右 + dfs(m, n, k, i + 1, j, visited); + dfs(m, n, k, i, j + 1, visited); + dfs(m, n, k, i - 1, j, visited); + dfs(m, n, k, i, j - 1, visited); + } +}; +``` + +```go +// by chatGPT (go) +func movingCount(m int, n int, k int) int { + visited := make([][]bool, m) + for i := 0; i < m; i++ { + visited[i] = make([]bool, n) + } + res := 0 + dfs(m, n, k, 0, 0, visited, &res) + return res +} + +func dfs(m, n, k, i, j int, visited [][]bool, res *int) { + if i < 0 || j < 0 || i >= m || j >= n { + return // 超出索引边界 + } + if i/10+i%10+j/10+j%10 > k { + return // 坐标和超出 k 的限制 + } + if visited[i][j] { + return // 之前已经访问过当前坐标 + } + *res++ + visited[i][j] = true + dfs(m, n, k, i+1, j, visited, res) + dfs(m, n, k, i, j+1, visited, res) + dfs(m, n, k, i-1, j, visited, res) + dfs(m, n, k, i, j-1, visited, res) +} +``` + +```java +// by labuladong (java) +class Solution { + public int movingCount(int m, int n, int k) { + boolean[][] visited = new boolean[m][n]; + dfs(m, n, k, 0, 0, visited); + return res; + } + + // 记录合法坐标数 + int res = 0; + + public void dfs(int m, int n, int k, int i, int j, boolean[][] visited) { + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + + if (i / 10 + i % 10 + j / 10 + j % 10 > k) { + // 坐标和超出 k 的限制 + return; + } + + if (visited[i][j]) { + // 之前已经访问过当前坐标 + return; + } + + // 走到一个合法坐标 + res++; + visited[i][j] = true; + + // DFS 遍历上下左右 + dfs(m, n, k, i + 1, j, visited); + dfs(m, n, k, i, j + 1, visited); + dfs(m, n, k, i - 1, j, visited); + dfs(m, n, k, i, j - 1, visited); + } +} +``` + +```javascript +// by chatGPT (javascript) +var movingCount = function(m, n, k) { + let visited = new Array(m).fill(false).map(() => new Array(n).fill(false)); + let res = 0; + + function dfs(m, n, k, i, j, visited) { + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + + if (Math.floor(i / 10) + i % 10 + Math.floor(j / 10) + j % 10 > k) { + // 坐标和超出 k 的限制 + return; + } + + if (visited[i][j]) { + // 之前已经访问过当前坐标 + return; + } + + // 走到一个合法坐标 + res++; + visited[i][j] = true; + + // DFS 遍历上下左右 + dfs(m, n, k, i + 1, j, visited); + dfs(m, n, k, i, j + 1, visited); + dfs(m, n, k, i - 1, j, visited); + dfs(m, n, k, i, j - 1, visited); + } + + dfs(m, n, k, 0, 0, visited); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def movingCount(self, m: int, n: int, k: int) -> int: + def dfs(i: int, j: int, visited: List[List[bool]]): + nonlocal res + if i < 0 or j < 0 or i >= m or j >= n: + # 超出索引边界 + return + + if i // 10 + i % 10 + j // 10 + j % 10 > k: + # 坐标和超出 k 的限制 + return + + if visited[i][j]: + # 之前已经访问过当前坐标 + return + + # 走到一个合法坐标 + res += 1 + visited[i][j] = True + + # DFS 遍历上下左右 + dfs(i + 1, j, visited) + dfs(i, j + 1, visited) + dfs(i - 1, j, visited) + dfs(i, j - 1, visited) + + # 记录合法坐标数 + res = 0 + visited = [[False] * n for _ in range(m)] + dfs(0, 0, visited) + return res +``` + +https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof 的多语言解法👆 + +https://leetcode.cn/problems/jump-game 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool canJump(vector& nums) { + int n = nums.size(); + int farthest = 0; + for (int i = 0; i < n - 1; i++) { + // 不断计算能跳到的最远距离 + farthest = max(farthest, i + nums[i]); + // 可能碰到了 0,卡住跳不动了 + if (farthest <= i) { + return false; + } + } + return farthest >= n - 1; + } +}; +``` + +```go +// by chatGPT (go) +func canJump(nums []int) bool { + n := len(nums) + farthest := 0 + for i := 0; i < n-1; i++ { + // 不断计算能跳到的最远距离 + farthest = max(farthest, i+nums[i]) + // 可能碰到了 0,卡住跳不动了 + if farthest <= i { + return false + } + } + return farthest >= n-1 +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean canJump(int[] nums) { + int n = nums.length; + int farthest = 0; + for (int i = 0; i < n - 1; i++) { + // 不断计算能跳到的最远距离 + farthest = Math.max(farthest, i + nums[i]); + // 可能碰到了 0,卡住跳不动了 + if (farthest <= i) { + return false; + } + } + return farthest >= n - 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var canJump = function(nums) { + let n = nums.length; + let farthest = 0; + for (let i = 0; i < n - 1; i++) { + // 不断计算能跳到的最远距离 + farthest = Math.max(farthest, i + nums[i]); + // 可能碰到了 0,卡住跳不动了 + if (farthest <= i) { + return false; + } + } + return farthest >= n - 1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def canJump(self, nums: List[int]) -> bool: + n = len(nums) + farthest = 0 + for i in range(n - 1): + # 不断计算能跳到的最远距离 + farthest = max(farthest, i + nums[i]) + # 可能碰到了 0,卡住跳不动了 + if farthest <= i: + return False + return farthest >= n - 1 +``` + +https://leetcode.cn/problems/jump-game 的多语言解法👆 + +https://leetcode.cn/problems/jump-game-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int jump(vector& nums) { + int n = nums.size(); + int end = 0, farthest = 0; + int jumps = 0; + for (int i = 0; i < n - 1; i++) { + farthest = max(nums[i] + i, farthest); + if (end == i) { + jumps++; + end = farthest; + } + } + return jumps; + } +}; +``` + +```go +// by chatGPT (go) +func jump(nums []int) int { + n := len(nums) + end, farthest, jumps := 0, 0, 0 + for i := 0; i < n-1; i++ { + farthest = max(nums[i]+i, farthest) + if end == i { + jumps++ + end = farthest + } + } + return jumps +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int jump(int[] nums) { + int n = nums.length; + int end = 0, farthest = 0; + int jumps = 0; + for (int i = 0; i < n - 1; i++) { + farthest = Math.max(nums[i] + i, farthest); + if (end == i) { + jumps++; + end = farthest; + } + } + return jumps; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number} + */ +var jump = function(nums) { + const n = nums.length; + let end = 0, farthest = 0, jumps = 0; + for (let i = 0; i < n - 1; i++) { + farthest = Math.max(nums[i] + i, farthest); + if (end === i) { + jumps++; + end = farthest; + } + } + return jumps; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def jump(self, nums: List[int]) -> int: + n = len(nums) + end, farthest, jumps = 0, 0, 0 + for i in range(n - 1): + farthest = max(nums[i] + i, farthest) + if end == i: + jumps += 1 + end = farthest + return jumps +``` + +https://leetcode.cn/problems/jump-game-ii 的多语言解法👆 + +https://leetcode.cn/problems/kill-process 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector killProcess(vector& pid, vector& ppid, int kill) { + // 构建多叉树,key 为父节点,value 为所有子节点的列表 + unordered_map> tree; + for (int i = 0; i < pid.size(); i++) { + int child = pid[i]; + int parent = ppid[i]; + tree[parent].push_back(child); + } + + vector res; + // 我这里用 BFS 算法遍历子树,删除以 kill 为根的所有子节点 + queue q; + q.push(kill); + while (!q.empty()) { + int cur = q.front(); + q.pop(); + res.push_back(cur); + if (tree.count(cur)) { + // 所有子节点入队 + for (int child : tree[cur]) { + q.push(child); + } + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func killProcess(pid []int, ppid []int, kill int) []int { + // 构建多叉树,key 为父节点,value 为所有子节点的列表 + tree := make(map[int][]int) + for i := 0; i < len(pid); i++ { + child := pid[i] + parent := ppid[i] + _, ok := tree[parent] + if !ok { + tree[parent] = make([]int, 0) + } + tree[parent] = append(tree[parent], child) + } + + res := make([]int, 0) + // 我这里用 BFS 算法遍历子树,删除以 kill 为根的所有子节点 + q := make([]int, 0) + q = append(q, kill) + for len(q) > 0 { + cur := q[0] + q = q[1:] + res = append(res, cur) + if children, ok := tree[cur]; ok { + // 所有子节点入队 + for _, child := range children { + q = append(q, child) + } + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List killProcess(List pid, List ppid, int kill) { + // 构建多叉树,key 为父节点,value 为所有子节点的列表 + HashMap> tree = new HashMap<>(); + for (int i = 0; i < pid.size(); i++) { + int child = pid.get(i); + int parent = ppid.get(i); + tree.putIfAbsent(parent, new ArrayList<>()); + tree.get(parent).add(child); + } + + List res = new LinkedList<>(); + // 我这里用 BFS 算法遍历子树,删除以 kill 为根的所有子节点 + Queue q = new LinkedList<>(); + q.offer(kill); + while (!q.isEmpty()) { + int cur = q.poll(); + res.add(cur); + if (tree.containsKey(cur)) { + // 所有子节点入队 + q.addAll(tree.get(cur)); + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var killProcess = function(pid, ppid, kill) { + // 构建多叉树,key 为父节点,value 为所有子节点的列表 + const tree = new Map(); + for (let i = 0; i < pid.length; i++) { + const child = pid[i]; + const parent = ppid[i]; + if (!tree.has(parent)) { + tree.set(parent, []); + } + tree.get(parent).push(child); + } + + const res = []; + // 我这里用 BFS 算法遍历子树,删除以 kill 为根的所有子节点 + const q = []; + q.push(kill); + while (q.length) { + const cur = q.shift(); + res.push(cur); + if (tree.has(cur)) { + // 所有子节点入队 + q.push(...tree.get(cur)); + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]: + # 构建多叉树,key 为父节点,value 为所有子节点的列表 + tree = {} + for i in range(len(pid)): + child = pid[i] + parent = ppid[i] + if parent not in tree: + tree[parent] = [] + tree[parent].append(child) + + res = [] + # 我这里用 BFS 算法遍历子树,删除以 kill 为根的所有子节点 + q = [] + q.append(kill) + while len(q) > 0: + cur = q.pop(0) + res.append(cur) + if cur in tree: + # 所有子节点入队 + q.extend(tree[cur]) + return res +``` + +https://leetcode.cn/problems/kill-process 的多语言解法👆 + +https://leetcode.cn/problems/koko-eating-bananas 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minEatingSpeed(vector& piles, int H) { + int left = 1; + int right = 1000000000 + 1; + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(piles, mid) <= H) { + right = mid; + } else { + left = mid + 1; + } + } + return left; + } + + // 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 + // f(x) 随着 x 的增加单调递减 + int f(vector& piles, int x) { + int hours = 0; + for (int i = 0; i < piles.size(); i++) { + hours += piles[i] / x; + if (piles[i] % x > 0) { + hours++; + } + } + return hours; + } +}; +``` + +```go +// by chatGPT (go) +func minEatingSpeed(piles []int, H int) int { + left, right := 1, 1000000000+1 + + for left < right { + mid := left + (right-left)/2 + if f(piles, mid) <= H { + right = mid + } else { + left = mid + 1 + } + } + + return left +} + +// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 +// f(x) 随着 x 的增加单调递减 +func f(piles []int, x int) int { + hours := 0 + for i := 0; i < len(piles); i++ { + hours += piles[i] / x + if piles[i]%x > 0 { + hours++ + } + } + return hours +} +``` + +```java +// by labuladong (java) +class Solution { + public int minEatingSpeed(int[] piles, int H) { + int left = 1; + int right = 1000000000 + 1; + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(piles, mid) <= H) { + right = mid; + } else { + left = mid + 1; + } + } + return left; + } + + // 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 + // f(x) 随着 x 的增加单调递减 + int f(int[] piles, int x) { + int hours = 0; + for (int i = 0; i < piles.length; i++) { + hours += piles[i] / x; + if (piles[i] % x > 0) { + hours++; + } + } + return hours; + } +} +``` + +```javascript +// by chatGPT (javascript) +var minEatingSpeed = function(piles, H) { + let left = 1; + let right = 1000000000 + 1; + + while (left < right) { + let mid = left + Math.floor((right - left) / 2); + if (f(piles, mid) <= H) { + right = mid; + } else { + left = mid + 1; + } + } + return left; +}; + +// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 +// f(x) 随着 x 的增加单调递减 +var f = function(piles, x) { + let hours = 0; + for (let i = 0; i < piles.length; i++) { + hours += Math.floor(piles[i] / x); + if (piles[i] % x > 0) { + hours++; + } + } + return hours; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minEatingSpeed(self, piles: List[int], H: int) -> int: + left = 1 + right = 1000000000 + 1 + + while left < right: + mid = left + (right - left) // 2 + if self.f(piles, mid) <= H: + right = mid + else: + left = mid + 1 + return left + + # 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 + # f(x) 随着 x 的增加单调递减 + def f(self, piles: List[int], x: int) -> int: + hours = 0 + for i in range(len(piles)): + hours += piles[i] // x + if piles[i] % x > 0: + hours += 1 + return hours +``` + +https://leetcode.cn/problems/koko-eating-bananas 的多语言解法👆 + +https://leetcode.cn/problems/kth-largest-element-in-a-stream 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class KthLargest { +private: + int k; + // 默认是小顶堆 + priority_queue, greater> pq; + +public: + KthLargest(int k, vector& nums) { + // 将 nums 装入小顶堆,保留下前 k 大的元素 + for (int e : nums) { + pq.push(e); + if (pq.size() > k) { + pq.pop(); + } + } + this->k = k; + } + + int add(int val) { + // 维护小顶堆只保留前 k 大的元素 + pq.push(val); + if (pq.size() > k) { + pq.pop(); + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return pq.top(); + } +}; +``` + +```go +// by chatGPT (go) +type KthLargest struct { + k int + pq *PriorityQueue +} + +func Constructor(k int, nums []int) KthLargest { + // 初始化优先队列 + pq := PriorityQueue{} + heap.Init(&pq) + + // 将 nums 装入小顶堆,保留下前 k 大的元素 + for _, e := range nums { + heap.Push(&pq, e) + if pq.Len() > k { + heap.Pop(&pq) + } + } + + return KthLargest{k: k, pq: &pq} +} + +func (this *KthLargest) Add(val int) int { + // 维护小顶堆只保留前 k 大的元素 + heap.Push(this.pq, val) + if this.pq.Len() > this.k { + heap.Pop(this.pq) + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return this.pq.Peek() +} + +type PriorityQueue []int + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i] < pq[j] +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + *pq = append(*pq, x.(int)) +} + +func (pq *PriorityQueue) Pop() interface{} { + n := len(*pq) + x := (*pq)[n-1] + *pq = (*pq)[:n-1] + return x +} + +func (pq *PriorityQueue) Peek() int { + return (*pq)[0] +} +``` + +```java +// by labuladong (java) +class KthLargest { + + private int k; + // 默认是小顶堆 + private PriorityQueue pq = new PriorityQueue<>(); + + public KthLargest(int k, int[] nums) { + // 将 nums 装入小顶堆,保留下前 k 大的元素 + for (int e : nums) { + pq.offer(e); + if (pq.size() > k) { + pq.poll(); + } + } + this.k = k; + } + + public int add(int val) { + // 维护小顶堆只保留前 k 大的元素 + pq.offer(val); + if (pq.size() > k) { + pq.poll(); + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return pq.peek(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var KthLargest = function(k, nums) { + this.k = k; + // 默认是小顶堆 + this.pq = new PriorityQueue(); + + // 将 nums 装入小顶堆,保留下前 k 大的元素 + for (var i = 0; i < nums.length; i++) { + this.pq.offer(nums[i]); + if (this.pq.size() > k) { + this.pq.poll(); + } + } +}; + +KthLargest.prototype.add = function(val) { + // 维护小顶堆只保留前 k 大的元素 + this.pq.offer(val); + if (this.pq.size() > this.k) { + this.pq.poll(); + } + // 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return this.pq.peek(); +}; + +// 实现一个小顶堆,自行实现添加和弹出方法 +class PriorityQueue { + constructor() { + this.data = []; + } + offer(val) { + this.data.push(val); + this.heapifyUp(); + } + poll() { + const first = this.data[0]; + const last = this.data.pop(); + if (this.data.length > 0) { + this.data[0] = last; + this.heapifyDown(); + } + return first; + } + size() { + return this.data.length; + } + heapifyUp() { + let i = this.data.length - 1; + while (i > 0) { + const parent = (i - 1) >> 1; + if (this.data[parent] > this.data[i]) { + this.swap(parent, i); + i = parent; + } else { + break; + } + } + } + heapifyDown() { + let i = 0; + const size = this.data.length; + while (i < size) { + let left = i * 2 + 1; + let right = i * 2 + 2; + if (left >= size) { + break; + } + let j = left; + if (right < size && this.data[right] < this.data[left]) { + j = right; + } + if (this.data[i] > this.data[j]) { + this.swap(i, j); + i = j; + } else { + break; + } + } + } + swap(i, j) { + [this.data[i], this.data[j]] = [this.data[j], this.data[i]]; + } +} +``` + +```python +# by chatGPT (python) +import queue + +class KthLargest: + + def __init__(self, k: int, nums: List[int]): + self.k = k + self.pq = queue.PriorityQueue() + # 将 nums 装入小顶堆,保留下前 k 大的元素 + for e in nums: + self.pq.put(e) + if self.pq.qsize() > k: + self.pq.get() + + def add(self, val: int) -> int: + # 维护小顶堆只保留前 k 大的元素 + self.pq.put(val) + if self.pq.qsize() > self.k: + self.pq.get() + # 堆顶就是第 k 大元素(即倒数第 k 小的元素) + return self.pq.queue[0] +``` + +https://leetcode.cn/problems/kth-largest-element-in-a-stream 的多语言解法👆 + +https://leetcode.cn/problems/kth-largest-element-in-an-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findKthLargest(vector& nums, int k) { + // 小顶堆,堆顶是最小元素 + priority_queue, greater> pq; + for (int e : nums) { + // 每个元素都要过一遍二叉堆 + pq.push(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.pop(); + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return pq.top(); + } +}; +``` + +```go +// by chatGPT (go) +func findKthLargest(nums []int, k int) int { + // 小顶堆,堆顶是最小元素 + pq := priorityQueue{} + for _, e := range nums { + // 每个元素都要过一遍二叉堆 + pq.offer(e) + // 堆中元素多于 k 个时,删除堆顶元素 + if pq.size() > k { + pq.poll() + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return pq.peek() +} + +type priorityQueue []int + +func (pq *priorityQueue) Len() int { return len(*pq) } + +func (pq *priorityQueue) Less(i, j int) bool { return (*pq)[i] < (*pq)[j] } + +func (pq *priorityQueue) Swap(i, j int) { (*pq)[i], (*pq)[j] = (*pq)[j], (*pq)[i] } + +func (pq *priorityQueue) Push(x interface{}) { *pq = append(*pq, x.(int)) } + +func (pq *priorityQueue) Pop() interface{} { + old := *pq + n := len(old) + x := old[n-1] + *pq = old[0 : n-1] + return x +} + +func (pq *priorityQueue) offer(e int) { heap.Push(pq, e) } + +func (pq *priorityQueue) poll() int { return heap.Pop(pq).(int) } + +func (pq *priorityQueue) peek() int { return (*pq)[0] } +``` + +```java +// by labuladong (java) +class Solution { + public int findKthLargest(int[] nums, int k) { + // 小顶堆,堆顶是最小元素 + PriorityQueue pq = new PriorityQueue<>(); + for (int e : nums) { + // 每个元素都要过一遍二叉堆 + pq.offer(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.poll(); + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return pq.peek(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var findKthLargest = function(nums, k) { + // 小顶堆,堆顶是最小元素 + let pq = new PriorityQueue(); + for (let e of nums) { + // 每个元素都要过一遍二叉堆 + pq.offer(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.poll(); + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return pq.peek(); +}; + +// JavaScript 的 PriorityQueue 实现可能不太一样,这里只是一个简单实现作为例子 +class PriorityQueue { + constructor() { + this.data = []; + } + offer(val) { + this.data.push(val); + this.bubbleUp(this.size() - 1); + } + poll() { + if (this.size() === 0) { + return undefined; + } + const result = this.data[0]; + const last = this.data.pop(); + if (this.size() !== 0) { + this.data[0] = last; + this.bubbleDown(0); + } + return result; + } + peek() { + return this.size() === 0 ? undefined : this.data[0]; + } + size() { + return this.data.length; + } + bubbleUp(index) { + while (index > 0) { + const parentIndex = Math.floor((index - 1) / 2); + if (this.data[index] >= this.data[parentIndex]) { + break; + } + this.swap(index, parentIndex); + index = parentIndex; + } + } + bubbleDown(index) { + while (STATUS_PASSED_TEST) { + const leftChildIndex = index * 2 + 1; + const rightChildIndex = index * 2 + 2; + let smallestChildIndex = index; + if (leftChildIndex < this.size() && this.data[leftChildIndex] < this.data[smallestChildIndex]) { + smallestChildIndex = leftChildIndex; + } + if (rightChildIndex < this.size() && this.data[rightChildIndex] < this.data[smallestChildIndex]) { + smallestChildIndex = rightChildIndex; + } + if (smallestChildIndex === index) { + break; + } + this.swap(index, smallestChildIndex); + index = smallestChildIndex; + } + } + swap(i, j) { + [this.data[i], this.data[j]] = [this.data[j], this.data[i]]; + } +} +``` + +```python +# by chatGPT (python) +import heapq + +class Solution: + def findKthLargest(self, nums: List[int], k: int) -> int: + # 小顶堆,堆顶是最小元素 + pq = [] + for e in nums: + # 每个元素都要过一遍二叉堆 + heapq.heappush(pq, e) + # 堆中元素多于 k 个时,删除堆顶元素 + if len(pq) > k: + heapq.heappop(pq) + # pq 中剩下的是 nums 中 k 个最大元素, + # 堆顶是最小的那个,即第 k 个最大元素 + return pq[0] +``` + +https://leetcode.cn/problems/kth-largest-element-in-an-array 的多语言解法👆 + +https://leetcode.cn/problems/kth-smallest-element-in-a-bst 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int kthSmallest(TreeNode* root, int k) { + // 利用 BST 的中序遍历特性 + traverse(root, k); + return res; + } + +private: + // 记录结果 + int res = 0; + // 记录当前元素的排名 + int rank = 0; + void traverse(TreeNode* root, int k) { + if (root == nullptr) { + return; + } + traverse(root->left, k); + /* 中序遍历代码位置 */ + rank++; + if (k == rank) { + // 找到第 k 小的元素 + res = root->val; + return; + } + /*****************/ + traverse(root->right, k); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +// kthSmallest returns the kth smallest element in the BST rooted with root. +func kthSmallest(root *TreeNode, k int) int { + // 记录结果 + var res int + // 记录当前元素的排名 + var rank int + + // traverse recursively traverses the BST rooted with root in-order + // and finds the rank-th smallest element in the BST. + // It updates rank and res accordingly. + var traverse func(root *TreeNode) + traverse = func(root *TreeNode) { + if root == nil { + return + } + traverse(root.Left) + /* 中序遍历代码位置 */ + rank++ + if k == rank { + // 找到第 k 小的元素 + res = root.Val + return + } + /*****************/ + traverse(root.Right) + } + + traverse(root) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int kthSmallest(TreeNode root, int k) { + // 利用 BST 的中序遍历特性 + traverse(root, k); + return res; + } + + // 记录结果 + int res = 0; + // 记录当前元素的排名 + int rank = 0; + void traverse(TreeNode root, int k) { + if (root == null) { + return; + } + traverse(root.left, k); + /* 中序遍历代码位置 */ + rank++; + if (k == rank) { + // 找到第 k 小的元素 + res = root.val; + return; + } + /*****************/ + traverse(root.right, k); + } +} +``` + +```javascript +// by chatGPT (javascript) +var kthSmallest = function(root, k) { + // 记录结果 + let res = 0; + // 记录当前元素的排名 + let rank = 0; + function traverse(root, k) { + if (root == null) { + return; + } + traverse(root.left, k); + /* 中序遍历代码位置 */ + rank++; + if (k == rank) { + // 找到第 k 小的元素 + res = root.val; + return; + } + /*****************/ + traverse(root.right, k); + } + + // 利用 BST 的中序遍历特性 + traverse(root, k); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def kthSmallest(self, root: TreeNode, k: int) -> int: + # 利用 BST 的中序遍历特性 + self.res = 0 + self.rank = 0 + self.traverse(root, k) + return self.res + + def traverse(self, root: TreeNode, k: int) -> None: + if root is None: + return + self.traverse(root.left, k) + # 中序遍历代码位置 + self.rank += 1 + if k == self.rank: + # 找到第 k 小的元素 + self.res = root.val + return + self.traverse(root.right, k) +``` + +https://leetcode.cn/problems/kth-smallest-element-in-a-bst 的多语言解法👆 + +https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int kthSmallest(vector>& matrix, int k) { + // 存储二元组 (matrix[i][j], i, j) + // i, j 记录当前元素的索引位置,用于生成下一个节点 + priority_queue, vector>, greater>> pq; + // 初始化优先级队列,把每一行的第一个元素装进去 + for (int i = 0; i < matrix.size(); i++) { + pq.push({matrix[i][0], i, 0}); + } + + int res = -1; + // 执行合并多个有序链表的逻辑,找到第 k 小的元素 + while (!pq.empty() && k > 0) { + auto cur = pq.top(); + pq.pop(); + res = cur[0]; + k--; + // 链表中的下一个节点加入优先级队列 + int i = cur[1], j = cur[2]; + if (j + 1 < matrix[i].size()) { + pq.push({matrix[i][j + 1], i, j + 1}); + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "container/heap" +) + +func kthSmallest(matrix [][]int, k int) int { + // 自定义一个最小堆类型 + pq := IntHeap{} + // 初始化堆,把每一行的第一个元素装进去 + for i := 0; i < len(matrix); i++ { + pq = append(pq, Item{value: matrix[i][0], row: i, col: 0}) + } + heap.Init(&pq) + + var res int + // 执行合并多个有序链表的逻辑,找到第 k 小的元素 + for k > 0 && pq.Len() > 0 { + cur := heap.Pop(&pq).(Item) + res = cur.value + k-- + // 链表中的下一个节点加入堆 + row, col := cur.row, cur.col+1 + if col < len(matrix[row]) { + heap.Push(&pq, Item{value: matrix[row][col], row: row, col: col}) + } + } + + return res +} + +// 定义一个 Item 类型,表示堆中的元素 +type Item struct { + value int // 当前元素的值 + row int // 当前元素所在的行 + col int // 当前元素所在的列 +} + +// 定义一个最小堆类型 IntHeap +// 实现 heap.Interface 接口的方法 +type IntHeap []Item + +func (t IntHeap) Len() int { + return len(t) +} + +func (t IntHeap) Less(i, j int) bool { + return t[i].value < t[j].value +} + +func (t IntHeap) Swap(i, j int) { + t[i], t[j] = t[j], t[i] +} + +func (t *IntHeap) Push(x interface{}) { + *t = append(*t, x.(Item)) +} + +func (t *IntHeap) Pop() interface{} { + n := len(*t) + x := (*t)[n-1] + *t = (*t)[:n-1] + return x +} +``` + +```java +// by labuladong (java) +class Solution { + public int kthSmallest(int[][] matrix, int k) { + // 存储二元组 (matrix[i][j], i, j) + // i, j 记录当前元素的索引位置,用于生成下一个节点 + PriorityQueue pq = new PriorityQueue<>((a, b) -> { + // 按照元素大小升序排序 + return a[0] - b[0]; + }); + + + // 初始化优先级队列,把每一行的第一个元素装进去 + for (int i = 0; i < matrix.length; i++) { + pq.offer(new int[]{matrix[i][0], i, 0}); + } + + int res = -1; + // 执行合并多个有序链表的逻辑,找到第 k 小的元素 + while (!pq.isEmpty() && k > 0) { + int[] cur = pq.poll(); + res = cur[0]; + k--; + // 链表中的下一个节点加入优先级队列 + int i = cur[1], j = cur[2]; + if (j + 1 < matrix[i].length) { + pq.add(new int[]{matrix[i][j + 1], i, j + 1}); + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var kthSmallest = function(matrix, k) { + // 存储二元组 (matrix[i][j], i, j) + // i, j 记录当前元素的索引位置,用于生成下一个节点 + const pq = new PriorityQueue((a, b) => { + // 按照元素大小升序排序 + return a[0] - b[0]; + }); + + + // 初始化优先级队列,把每一行的第一个元素装进去 + for (let i = 0; i < matrix.length; i++) { + pq.offer([matrix[i][0], i, 0]); + } + + let res = -1; + // 执行合并多个有序链表的逻辑,找到第 k 小的元素 + while (!pq.isEmpty() && k > 0) { + const cur = pq.poll(); + res = cur[0]; + k--; + // 链表中的下一个节点加入优先级队列 + const i = cur[1], j = cur[2]; + if (j + 1 < matrix[i].length) { + pq.offer([matrix[i][j + 1], i, j + 1]); + } + } + return res; +}; + +class PriorityQueue { + constructor(comparator) { + this.heap = []; + this.comparator = comparator; + } + + isEmpty() { + return this.heap.length === 0; + } + + offer(val) { + this.heap.push(val); + this.bubbleUp(this.heap.length - 1); + } + + poll() { + if (this.isEmpty()) { + return null; + } + const val = this.heap[0]; + const lastVal = this.heap.pop(); + if (this.heap.length > 0) { + this.heap[0] = lastVal; + this.sinkDown(0); + } + return val; + } + + bubbleUp(pos) { + while (pos > 0) { + const parentPos = (pos - 1) >>> 1; + if (this.comparator(this.heap[pos], this.heap[parentPos]) < 0) { + [this.heap[pos], this.heap[parentPos]] = [this.heap[parentPos], this.heap[pos]]; + pos = parentPos; + } else { + break; + } + } + } + + sinkDown(pos) { + const lastPos = this.heap.length - 1; + while (STATUS_PASSED_TEST) { + const leftChildPos = pos * 2 + 1; + const rightChildPos = pos * 2 + 2; + let minPos = pos; + if (leftChildPos <= lastPos && this.comparator(this.heap[leftChildPos], this.heap[minPos]) < 0) { + minPos = leftChildPos; + } + if (rightChildPos <= lastPos && this.comparator(this.heap[rightChildPos], this.heap[minPos]) < 0) { + minPos = rightChildPos; + } + if (minPos !== pos) { + [this.heap[pos], this.heap[minPos]] = [this.heap[minPos], this.heap[pos]]; + pos = minPos; + } else { + break; + } + } + } +}; +``` + +```python +# by chatGPT (python) +import heapq + +class Solution: + def kthSmallest(self, matrix: List[List[int]], k: int) -> int: + # 存储二元组 (matrix[i][j], i, j) + # i, j 记录当前元素的索引位置,用于生成下一个节点 + pq = [] + + # 初始化优先级队列,把每一行的第一个元素装进去 + for i in range(len(matrix)): + heapq.heappush(pq, [matrix[i][0], i, 0]) + + res = -1 + # 执行合并多个有序链表的逻辑,找到第 k 小的元素 + while len(pq) > 0 and k > 0: + cur = heapq.heappop(pq) + res = cur[0] + k -= 1 + # 链表中的下一个节点加入优先级队列 + i, j = cur[1], cur[2] + if j + 1 < len(matrix[i]): + heapq.heappush(pq, [matrix[i][j + 1], i, j + 1]) + return res +``` + +https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix 的多语言解法👆 + +https://leetcode.cn/problems/lMSNwu 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { + // 把链表元素转入栈中 + stack stk1, stk2; + while (l1 != nullptr) { + stk1.push(l1->val); + l1 = l1->next; + } + while (l2 != nullptr) { + stk2.push(l2->val); + l2 = l2->next; + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + ListNode* dummy = new ListNode(-1); + + // 记录进位 + int carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (!stk1.empty() || !stk2.empty() || carry > 0) { + // 先加上上次的进位 + int val = carry; + if (!stk1.empty()) { + val += stk1.top(); + stk1.pop(); + } + if (!stk2.empty()) { + val += stk2.top(); + stk2.pop(); + } + // 处理进位情况 + carry = val / 10; + val = val % 10; + // 构建新节点,直接接在 dummy 后面 + ListNode* newNode = new ListNode(val); + newNode->next = dummy->next; + dummy->next = newNode; + } + // 返回结果链表的头结点(去除虚拟头结点) + ListNode* retNode = dummy->next; + delete dummy; + return retNode; + } +}; +``` + +```go +// by chatGPT (go) +func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode { + // 把链表元素转入栈中 + stk1 := new(Stack) + for l1 != nil { + stk1.Push(l1.Val) + l1 = l1.Next + } + stk2 := new(Stack) + for l2 != nil { + stk2.Push(l2.Val) + l2 = l2.Next + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + dummy := &ListNode{Val: -1} + + // 记录进位 + carry := 0 + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + for !stk1.IsEmpty() || !stk2.IsEmpty() || carry > 0 { + // 先加上上次的进位 + val := carry + if !stk1.IsEmpty() { + val += stk1.Pop().(int) + } + if !stk2.IsEmpty() { + val += stk2.Pop().(int) + } + // 处理进位情况 + carry = val / 10 + val = val % 10 + // 构建新节点,直接接在 dummy 后面 + newNode := &ListNode{Val: val} + newNode.Next = dummy.Next + dummy.Next = newNode + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.Next +} + +// Stack 定义 +type Stack struct { + values []interface{} +} + +func (s *Stack) Push(val interface{}) { + s.values = append(s.values, val) +} + +func (s *Stack) Pop() interface{} { + if s.IsEmpty() { + return nil + } + lastIndex := len(s.values) - 1 + val := s.values[lastIndex] + s.values = s.values[:lastIndex] + return val +} + +func (s *Stack) IsEmpty() bool { + return len(s.values) == 0 +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + // 把链表元素转入栈中 + Stack stk1 = new Stack<>(); + while (l1 != null) { + stk1.push(l1.val); + l1 = l1.next; + } + Stack stk2 = new Stack<>(); + while (l2 != null) { + stk2.push(l2.val); + l2 = l2.next; + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + ListNode dummy = new ListNode(-1); + + // 记录进位 + int carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (!stk1.isEmpty() || !stk2.isEmpty() || carry > 0) { + // 先加上上次的进位 + int val = carry; + if (!stk1.isEmpty()) { + val += stk1.pop(); + } + if (!stk2.isEmpty()) { + val += stk2.pop(); + } + // 处理进位情况 + carry = val / 10; + val = val % 10; + // 构建新节点,直接接在 dummy 后面 + ListNode newNode = new ListNode(val); + newNode.next = dummy.next; + dummy.next = newNode; + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +var addTwoNumbers = function(l1, l2) { + // 把链表元素转入栈中 + const stk1 = []; + while (l1 !== null) { + stk1.push(l1.val); + l1 = l1.next; + } + const stk2 = []; + while (l2 !== null) { + stk2.push(l2.val); + l2 = l2.next; + } + + // 接下来基本上是复用我在第 2 题的代码逻辑 + // 注意新节点要直接插入到 dummy 后面 + + // 虚拟头结点(构建新链表时的常用技巧) + const dummy = new ListNode(-1); + + // 记录进位 + let carry = 0; + // 开始执行加法,两条链表走完且没有进位时才能结束循环 + while (stk1.length !== 0 || stk2.length !== 0 || carry > 0) { + // 先加上上次的进位 + let val = carry; + if (stk1.length !== 0) { + val += stk1.pop(); + } + if (stk2.length !== 0) { + val += stk2.pop(); + } + // 处理进位情况 + carry = Math.floor(val / 10); + val = val % 10; + // 构建新节点,直接接在 dummy 后面 + const newNode = new ListNode(val); + newNode.next = dummy.next; + dummy.next = newNode; + } + // 返回结果链表的头结点(去除虚拟头结点) + return dummy.next; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: + # 把链表元素转入栈中 + stk1 = [] + while l1: + stk1.append(l1.val) + l1 = l1.next + stk2 = [] + while l2: + stk2.append(l2.val) + l2 = l2.next + + # 接下来基本上是复用我在第 2 题的代码逻辑 + # 注意新节点要直接插入到 dummy 后面 + + # 虚拟头结点(构建新链表时的常用技巧) + dummy = ListNode(-1) + + # 记录进位 + carry = 0 + # 开始执行加法,两条链表走完且没有进位时才能结束循环 + while stk1 or stk2 or carry > 0: + # 先加上上次的进位 + val = carry + if stk1: + val += stk1.pop() + if stk2: + val += stk2.pop() + # 处理进位情况 + carry = val // 10 + val = val % 10 + # 构建新节点,直接接在 dummy 后面 + newNode = ListNode(val) + newNode.next = dummy.next + dummy.next = newNode + # 返回结果链表的头结点(去除虚拟头结点) + return dummy.next +``` + +https://leetcode.cn/problems/lMSNwu 的多语言解法👆 + +https://leetcode.cn/problems/letter-case-permutation 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string track; + vector res; + + vector letterCasePermutation(string s) { + backtrack(s, 0); + return res; + } + + void backtrack(string s, int index) { + if (index == s.size()) { + res.push_back(track); + return; + } + + if ('0' <= s[index] && s[index] <= '9') { + // s[index] 是数字 + // 做选择 + track.push_back(s[index]); + backtrack(s, index + 1); + // 撤销选择 + track.pop_back(); + } else { + // s[index] 是字母 + + // 小写字母,做选择 + track.push_back(tolower(s[index])); + backtrack(s, index + 1); + // 撤销选择 + track.pop_back(); + + // 大写字母,做选择 + track.push_back(toupper(s[index])); + backtrack(s, index + 1); + // 撤销选择 + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +func letterCasePermutation(s string) []string { + var track []byte + var res []string + + var backtrack func(s string, index int) + backtrack = func(s string, index int) { + if index == len(s) { + res = append(res, string(track)) + return + } + + if '0' <= s[index] && s[index] <= '9' { + // s[index] 是数字 + // 做选择 + track = append(track, s[index]) + backtrack(s, index + 1) + // 撤销选择 + track = track[:len(track)-1] + } else { + // s[index] 是字母 + + // 小写字母,做选择 + track = append(track, byte(unicode.ToLower(rune(s[index])))) + backtrack(s, index + 1) + // 撤销选择 + track = track[:len(track)-1] + + // 大写字母,做选择 + track = append(track, byte(unicode.ToUpper(rune(s[index])))) + backtrack(s, index + 1) + // 撤销选择 + track = track[:len(track)-1] + } + } + + backtrack(s, 0) + + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List letterCasePermutation(String s) { + backtrack(s, 0); + return res; + } + + StringBuilder track = new StringBuilder(); + List res = new LinkedList<>(); + + void backtrack(String s, int index) { + if (index == s.length()) { + res.add(track.toString()); + return; + } + + if ('0' <= s.charAt(index) && s.charAt(index) <= '9') { + // s[index] 是数字 + // 做选择 + track.append(s.charAt(index)); + backtrack(s, index + 1); + // 撤销选择 + track.deleteCharAt(track.length() - 1); + } else { + // s[index] 是字母 + + // 小写字母,做选择 + track.append(Character.toLowerCase(s.charAt(index))); + backtrack(s, index + 1); + // 撤销选择 + track.deleteCharAt(track.length() - 1); + + // 大写字母,做选择 + track.append(Character.toUpperCase(s.charAt(index))); + backtrack(s, index + 1); + // 撤销选择 + track.deleteCharAt(track.length() - 1); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var Solution = function() { + this.track = []; + this.res = []; + + this.letterCasePermutation = function(s) { + this.backtrack(s, 0); + return this.res; + } + + this.backtrack = function(s, index) { + if (index == s.length) { + this.res.push(this.track.join('')); + return; + } + + if ('0' <= s.charAt(index) && s.charAt(index) <= '9') { + // s[index] 是数字 + // 做选择 + this.track.push(s.charAt(index)); + this.backtrack(s, index + 1); + // 撤销选择 + this.track.pop(); + } else { + // s[index] 是字母 + + // 小写字母,做选择 + this.track.push(s.charAt(index).toLowerCase()); + this.backtrack(s, index + 1); + // 撤销选择 + this.track.pop(); + + // 大写字母,做选择 + this.track.push(s.charAt(index).toUpperCase()); + this.backtrack(s, index + 1); + // 撤销选择 + this.track.pop(); + } + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.track = "" + self.res = [] + + def letterCasePermutation(self, s: str) -> List[str]: + self.backtrack(s, 0) + return self.res + + def backtrack(self, s, index): + if index == len(s): + self.res.append(self.track) + return + + if '0' <= s[index] <= '9': + # s[index] 是数字 + # 做选择 + self.track += s[index] + self.backtrack(s, index + 1) + # 撤销选择 + self.track = self.track[:-1] + else: + # s[index] 是字母 + + # 小写字母,做选择 + self.track += s[index].lower() + self.backtrack(s, index + 1) + # 撤销选择 + self.track = self.track[:-1] + + # 大写字母,做选择 + self.track += s[index].upper() + self.backtrack(s, index + 1) + # 撤销选择 + self.track = self.track[:-1] +``` + +https://leetcode.cn/problems/letter-case-permutation 的多语言解法👆 + +https://leetcode.cn/problems/letter-combinations-of-a-phone-number 的多语言解法👇 + +```java +// by labuladong (java) +class Solution { + // 每个数字到字母的映射 + String[] mapping = new String[] { + "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" + }; + + List res = new LinkedList<>(); + StringBuilder sb = new StringBuilder(); + + public List letterCombinations(String digits) { + if (digits.isEmpty()) { + return res; + } + // 从 digits[0] 开始进行回溯 + backtrack(digits, 0); + return res; + } + + // 回溯算法主函数 + void backtrack(String digits, int start) { + if (sb.length() == digits.length()) { + // 到达回溯树底部 + res.add(sb.toString()); + return; + } + + // 回溯算法框架 + int digit = digits.charAt(start) - '0'; + for (char c : mapping[digit].toCharArray()) { + // 做选择 + sb.append(c); + // 递归下一层回溯树 + backtrack(digits, start + 1); + // 撤销选择 + sb.deleteCharAt(sb.length() - 1); + } + } +} +``` + +https://leetcode.cn/problems/letter-combinations-of-a-phone-number 的多语言解法👆 + +https://leetcode.cn/problems/letter-tile-possibilities 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int res = 0; + vector used; + + int numTilePossibilities(string s) { + vector nums(s.begin(), s.end()); + // 先排序,让相同的元素靠在一起 + sort(nums.begin(), nums.end()); + used = vector(nums.size(), false); + backtrack(nums); + return res - 1; + } + + void backtrack(vector& nums) { + res++; + + for (int i = 0; i < nums.size(); i++) { + if (used[i]) { + continue; + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + used[i] = true; + backtrack(nums); + used[i] = false; + } + } +}; +``` + +```go +// by chatGPT (go) +import ( + "sort" + "strings" +) + +func numTilePossibilities(s string) int { + nums := strings.Split(s, "") + // 先排序,让相同的元素靠在一起 + sort.Strings(nums) + + used := make([]bool, len(nums)) + res := 0 + + var backtrack func(nums []string) + backtrack = func(nums []string) { + res++ + + for i := 0; i < len(nums); i++ { + if used[i] { + continue + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if i > 0 && nums[i] == nums[i-1] && !used[i-1] { + continue + } + used[i] = true + backtrack(nums) + used[i] = false + } + } + backtrack(nums) + return res - 1 +} +``` + +```java +// by labuladong (java) +class Solution { + + int res = 0; + boolean[] used; + + public int numTilePossibilities(String s) { + char[] nums = s.toCharArray(); + // 先排序,让相同的元素靠在一起 + Arrays.sort(nums); + used = new boolean[nums.length]; + backtrack(nums); + return res - 1; + } + + void backtrack(char[] nums) { + res++; + + for (int i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + used[i] = true; + backtrack(nums); + used[i] = false; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var numTilePossibilities = function(s) { + var res = 0; + var nums = s.split('').sort(); + var used = new Array(nums.length).fill(false); + + function backtrack(nums) { + res++; + + for (var i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + // 做选择 + used[i] = true; + backtrack(nums); + // 撤销选择 + used[i] = false; + } + } + + backtrack(nums); + + return res - 1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + + def __init__(self): + self.res = 0 + self.used = [] + + def numTilePossibilities(self, s: str) -> int: + nums = list(s) + # 先排序,让相同的元素靠在一起 + nums.sort() + self.used = [False for _ in range(len(nums))] + self.backtrack(nums) + return self.res - 1 + + def backtrack(self, nums: list) -> None: + self.res += 1 + + for i in range(len(nums)): + if self.used[i]: + continue + # 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if i > 0 and nums[i] == nums[i - 1] and not self.used[i - 1]: + continue + self.used[i] = True + self.backtrack(nums) + self.used[i] = False +``` + +https://leetcode.cn/problems/letter-tile-possibilities 的多语言解法👆 + +https://leetcode.cn/problems/lfu-cache 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class LFUCache { + // key 到 val 的映射,我们后文称为 KV 表 + unordered_map keyToVal; + // key 到 freq 的映射,我们后文称为 KF 表 + unordered_map keyToFreq; + // freq 到 key 列表的映射,我们后文称为 FK 表 + unordered_map> freqToKeys; + // 记录最小的频次 + int minFreq; + // 记录 LFU 缓存的最大容量 + int cap; +public: + LFUCache(int capacity) { + keyToVal.clear(); + keyToFreq.clear(); + freqToKeys.clear(); + this->cap = capacity; + this->minFreq = 0; + } + + int get(int key) { + if (!keyToVal.count(key)) { + return -1; + } + // 增加 key 对应的 freq + increaseFreq(key); + return keyToVal[key]; + } + + void put(int key, int val) { + if (this->cap <= 0) return; + + /* 若 key 已存在,修改对应的 val 即可 */ + if (keyToVal.count(key)) { + keyToVal[key] = val; + // key 对应的 freq 加一 + increaseFreq(key); + return; + } + + /* key 不存在,需要插入 */ + /* 容量已满的话需要淘汰一个 freq 最小的 key */ + if (this->cap <= keyToVal.size()) { + removeMinFreqKey(); + } + + /* 插入 key 和 val,对应的 freq 为 1 */ + // 插入 KV 表 + keyToVal[key] = val; + // 插入 KF 表 + keyToFreq[key] = 1; + // 插入 FK 表 + freqToKeys[1].insert(key); + // 插入新 key 后最小的 freq 肯定是 1 + this->minFreq = 1; + } + +private: + void increaseFreq(int key) { + int freq = keyToFreq[key]; + /* 更新 KF 表 */ + keyToFreq[key] = freq + 1; + /* 更新 FK 表 */ + // 将 key 从 freq 对应的列表中删除 + freqToKeys[freq].erase(key); + // 将 key 加入 freq + 1 对应的列表中 + freqToKeys[freq + 1].insert(key); + // 如果 freq 对应的列表空了,移除这个 freq + if (freqToKeys[freq].empty()) { + freqToKeys.erase(freq); + // 如果这个 freq 恰好是 minFreq,更新 minFreq + if (freq == this->minFreq) { + this->minFreq++; + } + } + } + + void removeMinFreqKey() { + // freq 最小的 key 列表 + unordered_set keyList = freqToKeys[this->minFreq]; + // 其中最先被插入的那个 key 就是该被淘汰的 key + int deletedKey = *keyList.begin(); + /* 更新 FK 表 */ + keyList.erase(deletedKey); + if (keyList.empty()) { + freqToKeys.erase(this->minFreq); + // 问:这里需要更新 minFreq 的值吗? + } + /* 更新 KV 表 */ + keyToVal.erase(deletedKey); + /* 更新 KF 表 */ + keyToFreq.erase(deletedKey); + } +}; +``` + +```go +// by chatGPT (go) +type LFUCache struct { + // key 到 val 的映射,我们后文称为 KV 表 + keyToVal map[int]int + // key 到 freq 的映射,我们后文称为 KF 表 + keyToFreq map[int]int + // freq 到 key 列表的映射,我们后文称为 FK 表 + freqToKeys map[int]*linkedHashSet + // 记录最小的频次 + minFreq int + // 记录 LFU 缓存的最大容量 + cap int +} + +func Constructor(capacity int) LFUCache { + return LFUCache{ + keyToVal: make(map[int]int), + keyToFreq: make(map[int]int), + freqToKeys: make(map[int]*linkedHashSet), + cap: capacity, + minFreq: 0, + } +} + +func (this *LFUCache) Get(key int) int { + if _, ok := this.keyToVal[key]; !ok { + return -1 + } + // 增加 key 对应的 freq + this.increaseFreq(key) + return this.keyToVal[key] +} + +func (this *LFUCache) Put(key int, val int) { + if this.cap <= 0 { + return + } + + /* 若 key 已存在,修改对应的 val 即可 */ + if _, ok := this.keyToVal[key]; ok { + this.keyToVal[key] = val + // key 对应的 freq 加一 + this.increaseFreq(key) + return + } + + /* key 不存在,需要插入 */ + /* 容量已满的话需要淘汰一个 freq 最小的 key */ + if this.cap <= len(this.keyToVal) { + this.removeMinFreqKey() + } + + /* 插入 key 和 val,对应的 freq 为 1 */ + // 插入 KV 表 + this.keyToVal[key] = val + // 插入 KF 表 + this.keyToFreq[key] = 1 + // 插入 FK 表 + this.freqToKeys[1].add(key) + // 插入新 key 后最小的 freq 肯定是 1 + this.minFreq = 1 +} + +func (this *LFUCache) increaseFreq(key int) { + freq := this.keyToFreq[key] + /* 更新 KF 表 */ + this.keyToFreq[key] = freq + 1 + /* 更新 FK 表 */ + // 将 key 从 freq 对应的列表中删除 + this.freqToKeys[freq].remove(key) + // 将 key 加入 freq + 1 对应的列表中 + if this.freqToKeys[freq+1] == nil { + this.freqToKeys[freq+1] = newLinkedHashSet() + } + this.freqToKeys[freq+1].add(key) + // 如果 freq 对应的列表空了,移除这个 freq + if this.freqToKeys[freq].size() == 0 { + delete(this.freqToKeys, freq) + // 如果这个 freq 恰好是 minFreq,更新 minFreq + if freq == this.minFreq { + this.minFreq++ + } + } +} + +func (this *LFUCache) removeMinFreqKey() { + // freq 最小的 key 列表 + keyList := this.freqToKeys[this.minFreq] + // 其中最先被插入的那个 key 就是该被淘汰的 key + deletedKey := keyList.iterator().next() + /* 更新 FK 表 */ + keyList.remove(deletedKey) + if keyList.size() == 0 { + delete(this.freqToKeys, this.minFreq) + // 问:这里需要更新 minFreq 的值吗? + } + /* 更新 KV 表 */ + delete(this.keyToVal, deletedKey) + /* 更新 KF 表 */ + delete(this.keyToFreq, deletedKey) +} + +// 封装一个链表 +type linkedHashSet struct { + m map[int]*node + head *node + tail *node +} + +func newLinkedHashSet() *linkedHashSet { + head := &node{} + tail := &node{} + head.next = tail + tail.prev = head + return &linkedHashSet{ + m: make(map[int]*node), + head: head, + tail: tail, + } +} + +func (this *linkedHashSet) size() int { + return len(this.m) +} + +func (this *linkedHashSet) add(key int) { + if _, ok := this.m[key]; ok { + return + } + n := &node{key: key} + last := this.tail.prev + last.next = n + n.prev = last + n.next = this.tail + this.tail.prev = n + this.m[key] = n +} + +func (this *linkedHashSet) remove(key int) { + if n, ok := this.m[key]; ok { + n.prev.next = n.next + n.next.prev = n.prev + delete(this.m, key) + } +} + +func (this *linkedHashSet) iterator() *keyIterator { + return &keyIterator{this.head.next} +} + +type node struct { + key int + prev *node + next *node +} + +type keyIterator struct { + n *node +} + +func (this *keyIterator) hasNext() bool { + return this.n.next != nil +} + +func (this *keyIterator) next() int { + this.n = this.n.next + return this.n.key +} +``` + +```java +// by labuladong (java) +class LFUCache { + + // key 到 val 的映射,我们后文称为 KV 表 + HashMap keyToVal; + // key 到 freq 的映射,我们后文称为 KF 表 + HashMap keyToFreq; + // freq 到 key 列表的映射,我们后文称为 FK 表 + HashMap> freqToKeys; + // 记录最小的频次 + int minFreq; + // 记录 LFU 缓存的最大容量 + int cap; + + public LFUCache(int capacity) { + keyToVal = new HashMap<>(); + keyToFreq = new HashMap<>(); + freqToKeys = new HashMap<>(); + this.cap = capacity; + this.minFreq = 0; + } + + public int get(int key) { + if (!keyToVal.containsKey(key)) { + return -1; + } + // 增加 key 对应的 freq + increaseFreq(key); + return keyToVal.get(key); + } + + public void put(int key, int val) { + if (this.cap <= 0) return; + + /* 若 key 已存在,修改对应的 val 即可 */ + if (keyToVal.containsKey(key)) { + keyToVal.put(key, val); + // key 对应的 freq 加一 + increaseFreq(key); + return; + } + + /* key 不存在,需要插入 */ + /* 容量已满的话需要淘汰一个 freq 最小的 key */ + if (this.cap <= keyToVal.size()) { + removeMinFreqKey(); + } + + /* 插入 key 和 val,对应的 freq 为 1 */ + // 插入 KV 表 + keyToVal.put(key, val); + // 插入 KF 表 + keyToFreq.put(key, 1); + // 插入 FK 表 + freqToKeys.putIfAbsent(1, new LinkedHashSet<>()); + freqToKeys.get(1).add(key); + // 插入新 key 后最小的 freq 肯定是 1 + this.minFreq = 1; + } + + private void increaseFreq(int key) { + int freq = keyToFreq.get(key); + /* 更新 KF 表 */ + keyToFreq.put(key, freq + 1); + /* 更新 FK 表 */ + // 将 key 从 freq 对应的列表中删除 + freqToKeys.get(freq).remove(key); + // 将 key 加入 freq + 1 对应的列表中 + freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<>()); + freqToKeys.get(freq + 1).add(key); + // 如果 freq 对应的列表空了,移除这个 freq + if (freqToKeys.get(freq).isEmpty()) { + freqToKeys.remove(freq); + // 如果这个 freq 恰好是 minFreq,更新 minFreq + if (freq == this.minFreq) { + this.minFreq++; + } + } + } + + private void removeMinFreqKey() { + // freq 最小的 key 列表 + LinkedHashSet keyList = freqToKeys.get(this.minFreq); + // 其中最先被插入的那个 key 就是该被淘汰的 key + int deletedKey = keyList.iterator().next(); + /* 更新 FK 表 */ + keyList.remove(deletedKey); + if (keyList.isEmpty()) { + freqToKeys.remove(this.minFreq); + // 问:这里需要更新 minFreq 的值吗? + } + /* 更新 KV 表 */ + keyToVal.remove(deletedKey); + /* 更新 KF 表 */ + keyToFreq.remove(deletedKey); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} capacity + */ +var LFUCache = function(capacity) { + // key 到 val 的映射,我们后文称为 KV 表 + this.keyToVal = new Map(); + // key 到 freq 的映射,我们后文称为 KF 表 + this.keyToFreq = new Map(); + // freq 到 key 列表的映射,我们后文称为 FK 表 + this.freqToKeys = new Map(); + // 记录最小的频次 + this.minFreq = 0; + // 记录 LFU 缓存的最大容量 + this.cap = capacity; +}; + +/** + * @param {number} key + * @return {number} + */ +LFUCache.prototype.get = function(key) { + if (!this.keyToVal.has(key)) { + return -1; + } + // 增加 key 对应的 freq + this.increaseFreq(key); + return this.keyToVal.get(key); +}; + +/** + * @param {number} key + * @param {number} val + * @return {void} + */ +LFUCache.prototype.put = function(key, val) { + if (this.cap <= 0) return; + + /* 若 key 已存在,修改对应的 val 即可 */ + if (this.keyToVal.has(key)) { + this.keyToVal.set(key, val); + // key 对应的 freq 加一 + this.increaseFreq(key); + return; + } + + /* key 不存在,需要插入 */ + /* 容量已满的话需要淘汰一个 freq 最小的 key */ + if (this.cap <= this.keyToVal.size) { + this.removeMinFreqKey(); + } + + /* 插入 key 和 val,对应的 freq 为 1 */ + // 插入 KV 表 + this.keyToVal.set(key, val); + // 插入 KF 表 + this.keyToFreq.set(key, 1); + // 插入 FK 表 + if (!this.freqToKeys.has(1)) { + this.freqToKeys.set(1, new Set()); + } + this.freqToKeys.get(1).add(key); + // 插入新 key 后最小的 freq 肯定是 1 + this.minFreq = 1; +}; + +/** + * @param {number} key + * @return {void} + */ +LFUCache.prototype.increaseFreq = function(key) { + let freq = this.keyToFreq.get(key); + /* 更新 KF 表 */ + this.keyToFreq.set(key, freq + 1); + /* 更新 FK 表 */ + // 将 key 从 freq 对应的列表中删除 + this.freqToKeys.get(freq).delete(key); + // 将 key 加入 freq + 1 对应的列表中 + if (!this.freqToKeys.has(freq + 1)) { + this.freqToKeys.set(freq + 1, new Set()); + } + this.freqToKeys.get(freq + 1).add(key); + // 如果 freq 对应的列表空了,移除这个 freq + if (this.freqToKeys.get(freq).size === 0) { + this.freqToKeys.delete(freq); + // 如果这个 freq 恰好是 minFreq,更新 minFreq + if (freq === this.minFreq) { + this.minFreq++; + } + } +} + +/** + * @return {void} + */ +LFUCache.prototype.removeMinFreqKey = function() { + // freq 最小的 key 列表 + let keyList = this.freqToKeys.get(this.minFreq); + // 其中最先被插入的那个 key 就是该被淘汰的 key + let deletedKey = keyList.values().next().value; + /* 更新 FK 表 */ + keyList.delete(deletedKey); + if (keyList.size === 0) { + this.freqToKeys.delete(this.minFreq); + // 问:这里需要更新 minFreq 的值吗? + } + /* 更新 KV 表 */ + this.keyToVal.delete(deletedKey); + /* 更新 KF 表 */ + this.keyToFreq.delete(deletedKey); +}; +``` + +```python +# by chatGPT (python) +class LFUCache: + + def __init__(self, capacity: int): + # key 到 val 的映射,我们后文称为 KV 表 + self.keyToVal = {} + # key 到 freq 的映射,我们后文称为 KF 表 + self.keyToFreq = {} + # freq 到 key 列表的映射,我们后文称为 FK 表 + self.freqToKeys = {} + # 记录最小的频次 + self.minFreq = 0 + # 记录 LFU 缓存的最大容量 + self.cap = capacity + + def get(self, key: int) -> int: + if key not in self.keyToVal: + return -1 + # 增加 key 对应的 freq + self.__increaseFreq(key) + return self.keyToVal[key] + + def put(self, key: int, val: int) -> None: + if self.cap <= 0: + return + + # 若 key 已存在,修改对应的 val 即可 + if key in self.keyToVal: + self.keyToVal[key] = val + # key 对应的 freq 加一 + self.__increaseFreq(key) + return + + # key 不存在,需要插入 + # 容量已满的话需要淘汰一个 freq 最小的 key + if self.cap <= len(self.keyToVal): + self.__removeMinFreqKey() + + # 插入 key 和 val,对应的 freq 为 1 + # 插入 KV 表 + self.keyToVal[key] = val + # 插入 KF 表 + self.keyToFreq[key] = 1 + # 插入 FK 表 + self.freqToKeys.setdefault(1, set()) + self.freqToKeys[1].add(key) + # 插入新 key 后最小的 freq 肯定是 1 + self.minFreq = 1 + + def __increaseFreq(self, key: int): + freq = self.keyToFreq[key] + # 更新 KF 表 + self.keyToFreq[key] = freq + 1 + # 更新 FK 表 + # 将 key 从 freq 对应的列表中删除 + self.freqToKeys[freq].remove(key) + # 将 key 加入 freq + 1 对应的列表中 + self.freqToKeys.setdefault(freq + 1, set()) + self.freqToKeys[freq + 1].add(key) + # 如果 freq 对应的列表空了,移除这个 freq + if not self.freqToKeys[freq]: + del self.freqToKeys[freq] + # 如果这个 freq 恰好是 minFreq,更新 minFreq + if freq == self.minFreq: + self.minFreq += 1 + + def __removeMinFreqKey(self): + # freq 最小的 key 列表 + keyList = self.freqToKeys[self.minFreq] + # 其中最先被插入的那个 key 就是该被淘汰的 key + deletedKey = keyList.pop() + # 更新 FK 表 + if not keyList: + del self.freqToKeys[self.minFreq] + # 更新 KV 表 + del self.keyToVal[deletedKey] + # 更新 KF 表 + del self.keyToFreq[deletedKey] +``` + +https://leetcode.cn/problems/lfu-cache 的多语言解法👆 + +https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxValue(vector>& grid) { + int m = grid.size(); + int n = grid[0].size(); + vector> dp(m, vector(n, 0)); + + /**** base case ****/ + dp[0][0] = grid[0][0]; + for (int i = 1; i < m; i++) + dp[i][0] = dp[i - 1][0] + grid[i][0]; + + for (int j = 1; j < n; j++) + dp[0][j] = dp[0][j - 1] + grid[0][j]; + + // 状态转移 + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = max( + dp[i - 1][j], + dp[i][j - 1] + ) + grid[i][j]; + } + } + return dp[m - 1][n - 1]; + } +}; +``` + +```go +// by chatGPT (go) +func maxValue(grid [][]int) int { + m := len(grid) + n := len(grid[0]) + dp := make([][]int, m) + for i := range dp { + dp[i] = make([]int, n) + } + + /**** base case ****/ + dp[0][0] = grid[0][0] + for i := 1; i < m; i++ { + dp[i][0] = dp[i-1][0] + grid[i][0] + } + + for j := 1; j < n; j++ { + dp[0][j] = dp[0][j-1] + grid[0][j] + } + + // 状态转移 + for i := 1; i < m; i++ { + for j := 1; j < n; j++ { + dp[i][j] = max( + dp[i-1][j], + dp[i][j-1], + ) + grid[i][j] + } + } + return dp[m-1][n-1] +} + +func max(nums ...int) int { + res := nums[0] + for _, num := range nums[1:] { + if num > res { + res = num + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxValue(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + int[][] dp = new int[m][n]; + + /**** base case ****/ + dp[0][0] = grid[0][0]; + for (int i = 1; i < m; i++) + dp[i][0] = dp[i - 1][0] + grid[i][0]; + + for (int j = 1; j < n; j++) + dp[0][j] = dp[0][j - 1] + grid[0][j]; + + // 状态转移 + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = Math.max( + dp[i - 1][j], + dp[i][j - 1] + ) + grid[i][j]; + } + } + return dp[m - 1][n - 1]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxValue = function(grid) { + let m = grid.length; + let n = grid[0].length; + let dp = new Array(m).fill().map(() => new Array(n).fill(0)); + + /**** base case ****/ + dp[0][0] = grid[0][0]; + for (let i = 1; i < m; i++) + dp[i][0] = dp[i - 1][0] + grid[i][0]; + + for (let j = 1; j < n; j++) + dp[0][j] = dp[0][j - 1] + grid[0][j]; + + // 状态转移 + for (let i = 1; i < m; i++) { + for (let j = 1; j < n; j++) { + dp[i][j] = Math.max( + dp[i - 1][j], + dp[i][j - 1] + ) + grid[i][j]; + } + } + return dp[m - 1][n - 1]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxValue(self, grid: List[List[int]]) -> int: + m = len(grid) + n = len(grid[0]) + dp = [[0] * n for _ in range(m)] + + # base case + dp[0][0] = grid[0][0] + for i in range(1, m): + dp[i][0] = dp[i - 1][0] + grid[i][0] + + for j in range(1, n): + dp[0][j] = dp[0][j - 1] + grid[0][j] + + # 状态转移 + for i in range(1, m): + for j in range(1, n): + dp[i][j] = max( + dp[i - 1][j], + dp[i][j - 1] + ) + grid[i][j] + + return dp[m - 1][n - 1] +``` + +https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxSubArray(vector& nums) { + int n = nums.size(); + if (n == 0) return 0; + vector dp(n); + // base case + // 第一个元素前面没有子数组 + dp[0] = nums[0]; + // 状态转移方程 + for (int i = 1; i < n; i++) { + dp[i] = max(nums[i], nums[i] + dp[i - 1]); + } + // 得到 nums 的最大子数组 + int res = INT_MIN; + for (int i = 0; i < n; i++) { + res = max(res, dp[i]); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func maxSubArray(nums []int) int { + n := len(nums) + if n == 0 { + return 0 + } + + dp := make([]int, n) + + // base case + dp[0] = nums[0] + + // 状态转移方程 + for i := 1; i < n; i++ { + dp[i] = func() int { + if nums[i] > nums[i]+dp[i-1] { + return nums[i] + } + return nums[i] + dp[i-1] + }() + } + + // 得到 nums 的最大子数组 + res := math.MinInt32 + for i := 0; i < n; i++ { + res = func() int { + if res > dp[i] { + return res + } + return dp[i] + }() + } + + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxSubArray(int[] nums) { + int n = nums.length; + if (n == 0) return 0; + int[] dp = new int[n]; + // base case + // 第一个元素前面没有子数组 + dp[0] = nums[0]; + // 状态转移方程 + for (int i = 1; i < n; i++) { + dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]); + } + // 得到 nums 的最大子数组 + int res = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + res = Math.max(res, dp[i]); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxSubArray = function(nums) { + const n = nums.length; + if (n == 0) return 0; + const dp = new Array(n).fill(0); + // base case + // 第一个元素前面没有子数组 + dp[0] = nums[0]; + // 状态转移方程 + for (let i = 1; i < n; i++) { + dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]); + } + // 得到 nums 的最大子数组 + let res = Number.MIN_SAFE_INTEGER; + for (let i = 0; i < n; i++) { + res = Math.max(res, dp[i]); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + n = len(nums) + if n == 0: + return 0 + dp = [0] * n + # base case + # 第一个元素前面没有子数组 + dp[0] = nums[0] + # 状态转移方程 + for i in range(1, n): + dp[i] = max(nums[i], nums[i] + dp[i - 1]) + # 得到 nums 的最大子数组 + res = float('-inf') + for i in range(n): + res = max(res, dp[i]) + return res +``` + +https://leetcode.cn/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof 的多语言解法👆 + +https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + ListNode* p1 = headA; + ListNode* p2 = headB; + while (p1 != p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 == nullptr) { + p1 = headB; + } else { + p1 = p1->next; + } + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 == nullptr) { + p2 = headA; + } else { + p2 = p2->next; + } + } + return p1; + } +}; +``` + +```go +// by chatGPT (go) +func getIntersectionNode(headA, headB *ListNode) *ListNode { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + p1, p2 := headA, headB + for p1 != p2 { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if p1 == nil { + p1 = headB + } else { + p1 = p1.Next + } + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if p2 == nil { + p2 = headA + } else { + p2 = p2.Next + } + } + return p1 +} +``` + +```java +// by labuladong (java) +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + ListNode p1 = headA, p2 = headB; + while (p1 != p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 == null) p1 = headB; + else p1 = p1.next; + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 == null) p2 = headA; + else p2 = p2.next; + } + return p1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var getIntersectionNode = function(headA, headB) { + // p1 指向 A 链表头结点,p2 指向 B 链表头结点 + let p1 = headA, p2 = headB; + while (p1 != p2) { + // p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if (p1 == null) { + p1 = headB; + } else { + p1 = p1.next; + } + // p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if (p2 == null) { + p2 = headA; + } else { + p2 = p2.next; + } + } + return p1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: + # p1 指向 A 链表头结点,p2 指向 B 链表头结点 + p1, p2 = headA, headB + while p1 != p2: + # p1 走一步,如果走到 A 链表末尾,转到 B 链表 + if p1 == None: + p1 = headB + else: + p1 = p1.next + # p2 走一步,如果走到 B 链表末尾,转到 A 链表 + if p2 == None: + p2 = headA + else: + p2 = p2.next + return p1 +``` + +https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof 的多语言解法👆 + +https://leetcode.cn/problems/linked-list-cycle 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool hasCycle(ListNode *head) { + // 快慢指针初始化指向 head + ListNode *slow = head, *fast = head; + // 快指针走到末尾时停止 + while (fast != nullptr && fast->next != nullptr) { + // 慢指针走一步,快指针走两步 + slow = slow->next; + fast = fast->next->next; + // 快慢指针相遇,说明含有环 + if (slow == fast) { + return true; + } + } + // 不包含环 + return false; + } +}; +``` + +```go +// by chatGPT (go) +func hasCycle(head *ListNode) bool { + // 快慢指针初始化指向 head + slow, fast := head, head + // 快指针走到末尾时停止 + for fast != nil && fast.Next != nil { + // 慢指针走一步,快指针走两步 + slow = slow.Next + fast = fast.Next.Next + // 快慢指针相遇,说明含有环 + if slow == fast { + return true + } + } + // 不包含环 + return false +} +``` + +```java +// by labuladong (java) +public class Solution { + public boolean hasCycle(ListNode head) { + // 快慢指针初始化指向 head + ListNode slow = head, fast = head; + // 快指针走到末尾时停止 + while (fast != null && fast.next != null) { + // 慢指针走一步,快指针走两步 + slow = slow.next; + fast = fast.next.next; + // 快慢指针相遇,说明含有环 + if (slow == fast) { + return true; + } + } + // 不包含环 + return false; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} head + * @return {boolean} + */ +var hasCycle = function(head) { + // 快慢指针初始化指向 head + let slow = head, fast = head; + // 快指针走到末尾时停止 + while (fast !== null && fast.next !== null) { + // 慢指针走一步,快指针走两步 + slow = slow.next; + fast = fast.next.next; + // 快慢指针相遇,说明含有环 + if (slow === fast) { + return true; + } + } + // 不包含环 + return false; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def hasCycle(self, head: ListNode) -> bool: + # 快慢指针初始化指向 head + slow, fast = head, head + # 快指针走到末尾时停止 + while fast and fast.next: + # 慢指针走一步,快指针走两步 + slow = slow.next + fast = fast.next.next + # 快慢指针相遇,说明含有环 + if slow == fast: + return True + # 不包含环 + return False +``` + +https://leetcode.cn/problems/linked-list-cycle 的多语言解法👆 + +https://leetcode.cn/problems/linked-list-cycle-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode *detectCycle(ListNode *head) { + ListNode *fast, *slow; + fast = slow = head; + while (fast != nullptr && fast->next != nullptr) { + fast = fast->next->next; + slow = slow->next; + if (fast == slow) break; + /** + ![](../pictures/双指针/3.jpeg) + */ + } + // 上面的代码类似 hasCycle 函数 + if (fast == nullptr || fast->next == nullptr) { + // fast 遇到空指针说明没有环 + return nullptr; + } + + // 重新指向头结点 + slow = head; + /** + ![](../pictures/双指针/2.jpeg) + */ + // 快慢指针同步前进,相交点就是环起点 + while (slow != fast) { + fast = fast->next; + slow = slow->next; + } + return slow; + } +}; +``` + +```go +// by chatGPT (go) +func detectCycle(head *ListNode) *ListNode { + fast, slow := head, head + for fast != nil && fast.Next != nil { + fast = fast.Next.Next + slow = slow.Next + if fast == slow { + break + /** + ![](../pictures/双指针/3.jpeg) + */ + } + } + if fast == nil || fast.Next == nil { + return nil + } + slow = head + /** + ![](../pictures/双指针/2.jpeg) + */ + for slow != fast { + slow = slow.Next + fast = fast.Next + } + return slow +} +``` + +```java +// by labuladong (java) +public class Solution { + public ListNode detectCycle(ListNode head) { + ListNode fast, slow; + fast = slow = head; + while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; + if (fast == slow) break; + /** + ![](../pictures/双指针/3.jpeg) + */ + } + // 上面的代码类似 hasCycle 函数 + if (fast == null || fast.next == null) { + // fast 遇到空指针说明没有环 + return null; + } + + // 重新指向头结点 + slow = head; + /** + ![](../pictures/双指针/2.jpeg) + */ + // 快慢指针同步前进,相交点就是环起点 + while (slow != fast) { + fast = fast.next; + slow = slow.next; + } + return slow; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} head + * @return {ListNode} + */ +var detectCycle = function(head) { + let fast = head, slow = head; + while (fast !== null && fast.next !== null) { + fast = fast.next.next; + slow = slow.next; + if (fast === slow) { + break; + /** + ![](../pictures/双指针/3.jpeg) + */ + } + } + // 上面的代码类似 hasCycle 函数 + if (fast === null || fast.next === null) { + // fast 遇到空指针说明没有环 + return null; + } + + // 重新指向头结点 + slow = head; + /** + ![](../pictures/双指针/2.jpeg) + */ + // 快慢指针同步前进,相交点就是环起点 + while (slow !== fast) { + fast = fast.next; + slow = slow.next; + } + return slow; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def detectCycle(self, head: ListNode) -> ListNode: + fast = slow = head + while fast and fast.next: + fast = fast.next.next + slow = slow.next + if fast == slow: + break + """ + + ![](../pictures/双指针/3.jpeg) + """ + if not fast or not fast.next: + return None + + slow = head + """ + + ![](../pictures/双指针/2.jpeg) + """ + while slow != fast: + slow = slow.next + fast = fast.next + + return slow +``` + +https://leetcode.cn/problems/linked-list-cycle-ii 的多语言解法👆 + +https://leetcode.cn/problems/linked-list-random-node 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + ListNode* head; + std::random_device rd; +public: + Solution(ListNode* head) { + this->head = head; + } + + /* 返回链表中一个随机节点的值 */ + int getRandom() { + int i = 0, res = 0; + ListNode* p = head; + // while 循环遍历链表 + while (p != nullptr) { + i++; + // 生成一个 [0, i) 之间的整数 + // 这个整数等于 0 的概率就是 1/i + if (0 == rd() % i) { + res = p->val; + } + p = p->next; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +type Solution struct { + head *ListNode + r *rand.Rand +} + +/** @param head The linked list's head. + Note that the head is guaranteed to be not null, so it contains at least one node. */ +func Constructor(head *ListNode) Solution { + return Solution{ + head: head, + r: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} + +/** Returns a random node's value. */ +func (this *Solution) GetRandom() int { + i := 0 + res := 0 + p := this.head + // while 循环遍历链表 + for p != nil { + i++ + // 生成一个 [0, i) 之间的整数 + // 这个整数等于 0 的概率就是 1/i + if 0 == this.r.Intn(i) { + res = p.Val + } + p = p.Next + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + + ListNode head; + Random r = new Random(); + + public Solution(ListNode head) { + this.head = head; + } + + /* 返回链表中一个随机节点的值 */ + int getRandom() { + int i = 0, res = 0; + ListNode p = head; + // while 循环遍历链表 + while (p != null) { + i++; + // 生成一个 [0, i) 之间的整数 + // 这个整数等于 0 的概率就是 1/i + if (0 == r.nextInt(i)) { + res = p.val; + } + p = p.next; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} head + * @return {number} + */ +var Solution = function(head) { + this.head = head; + this.r = new Random(); +}; + +/** + * Returns a random node's value. + * @return {number} + */ +Solution.prototype.getRandom = function() { + let i = 0; + let res = 0; + let p = this.head; + // while 循环遍历链表 + while (p !== null) { + i++; + // 生成一个 [0, i) 之间的整数 + // 这个整数等于 0 的概率就是 1/i + if (0 === this.r.nextInt(i)) { + res = p.val; + } + p = p.next; + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + + def __init__(self, head: ListNode): + self.head = head + self.r = random.Random() + + def getRandom(self) -> int: + i, res = 0, 0 + p = self.head + # while 循环遍历链表 + while p: + i += 1 + # 生成一个 [0, i) 之间的整数 + # 这个整数等于 0 的概率就是 1/i + if 0 == self.r.randint(0, i-1): + res = p.val + p = p.next + return res +``` + +https://leetcode.cn/problems/linked-list-random-node 的多语言解法👆 + +https://leetcode.cn/problems/longest-common-prefix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string longestCommonPrefix(vector& strs) { + int m = strs.size(); + int n = strs[0].length(); + for (int col = 0; col < n; col++) { + for (int row = 1; row < m; row++) { + string thisStr = strs[row], prevStr = strs[row - 1]; + // 判断每个字符串的 col 索引是否都相同 + if (col >= thisStr.length() || col >= prevStr.length() || + thisStr.at(col) != prevStr.at(col)) { + // 发现不匹配的字符,只有 strs[row][0..col-1] 是公共前缀 + return strs[row].substr(0, col); + } + } + } + return strs[0]; + } +}; +``` + +```go +// by chatGPT (go) +func longestCommonPrefix(strs []string) string { + m := len(strs) + // 以第一行的列数为基准 + n := len(strs[0]) + for col := 0; col < n; col++ { + for row := 1; row < m; row++ { + thisStr, prevStr := strs[row], strs[row-1] + // 判断每个字符串的 col 索引是否都相同 + if col >= len(thisStr) || col >= len(prevStr) || + thisStr[col] != prevStr[col] { + // 发现不匹配的字符,只有 strs[row][0..col-1] 是公共前缀 + return strs[row][:col] + } + } + } + return strs[0] +} +``` + +```java +// by labuladong (java) +class Solution { + public String longestCommonPrefix(String[] strs) { + int m = strs.length; + // 以第一行的列数为基准 + int n = strs[0].length(); + for (int col = 0; col < n; col++) { + for (int row = 1; row < m; row++) { + String thisStr = strs[row], prevStr = strs[row - 1]; + // 判断每个字符串的 col 索引是否都相同 + if (col >= thisStr.length() || col >= prevStr.length() || + thisStr.charAt(col) != prevStr.charAt(col)) { + // 发现不匹配的字符,只有 strs[row][0..col-1] 是公共前缀 + return strs[row].substring(0, col); + } + } + } + return strs[0]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestCommonPrefix = function(strs) { + let m = strs.length; + // 以第一行的列数为基准 + let n = strs[0].length; + for (let col = 0; col < n; col++) { + for (let row = 1; row < m; row++) { + let thisStr = strs[row], prevStr = strs[row - 1]; + // 判断每个字符串的 col 索引是否都相同 + if (col >= thisStr.length || col >= prevStr.length || + thisStr.charAt(col) != prevStr.charAt(col)) { + // 发现不匹配的字符,只有 strs[row][0..col-1] 是公共前缀 + return strs[row].substring(0, col); + } + } + } + return strs[0]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def longestCommonPrefix(self, strs: List[str]) -> str: + m = len(strs) + # 以第一行的列数为基准 + n = len(strs[0]) + for col in range(n): + for row in range(1, m): + thisStr, prevStr = strs[row], strs[row - 1] + # 判断每个字符串的 col 索引是否都相同 + if col >= len(thisStr) or col >= len(prevStr) or thisStr[col] != prevStr[col]: + # 发现不匹配的字符,只有 strs[row][0..col-1] 是公共前缀 + return strs[row][:col] + return strs[0] +``` + +https://leetcode.cn/problems/longest-common-prefix 的多语言解法👆 + +https://leetcode.cn/problems/longest-common-subsequence 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int longestCommonSubsequence(string s1, string s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + int dp[m + 1][n + 1]; + // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + // base case: dp[0][..] = dp[..][0] = 0 + memset(dp, 0, sizeof(dp)); + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1[i - 1] == s2[j - 1]) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + + return dp[m][n]; + } +}; +``` + +```go +// by chatGPT (go) +func longestCommonSubsequence(s1 string, s2 string) int { + m, n := len(s1), len(s2) + //定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + //目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + //base case: dp[0][..] = dp[..][0] = 0 + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + //现在 i 和 j 从 1 开始,所以要减一 + if s1[i-1] == s2[j-1] { + //s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i-1][j-1] + } else { + //s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j-1], dp[i-1][j]) + } + } + } + + return dp[m][n] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int longestCommonSubsequence(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + int[][] dp = new int[m + 1][n + 1]; + // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + // base case: dp[0][..] = dp[..][0] = 0 + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + + return dp[m][n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestCommonSubsequence = function(s1, s2) { + const m = s1.length, n = s2.length; + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0)); + // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + // base case: dp[0][..] = dp[..][0] = 0 + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1.charAt(i - 1) === s2.charAt(j - 1)) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + + return dp[m][n]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def longestCommonSubsequence(self, s1: str, s2: str) -> int: + m, n = len(s1), len(s2) + # 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + dp = [[0] * (n + 1) for _ in range(m + 1)] + # 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + # base case: dp[0][..] = dp[..][0] = 0 + + for i in range(1, m + 1): + for j in range(1, n + 1): + # 现在 i 和 j 从 1 开始,所以要减一 + if s1[i - 1] == s2[j - 1]: + # s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1] + else: + # s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]) + + return dp[m][n] +``` + +https://leetcode.cn/problems/longest-common-subsequence 的多语言解法👆 + +https://leetcode.cn/problems/longest-consecutive-sequence 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int longestConsecutive(vector& nums) { + // 转化成哈希集合,方便快速查找是否存在某个元素 + unordered_set set; + for (int num : nums) { + set.insert(num); + } + + int res = 0; + + for (int num : set) { + if (set.count(num - 1)) { + // num 不是连续子序列的第一个,跳过 + continue; + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + int curNum = num; + int curLen = 1; + + while (set.count(curNum + 1)) { + curNum += 1; + curLen += 1; + } + // 更新最长连续序列的长度 + res = max(res, curLen); + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func longestConsecutive(nums []int) int { + // 转化成哈希集合,方便快速查找是否存在某个元素 + set := make(map[int]bool) + for _, num := range nums { + set[num] = true + } + + res := 0 + + for num := range set { + if set[num-1] { + // num 不是连续子序列的第一个,跳过 + continue + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + curNum := num + curLen := 1 + + for set[curNum+1] { + curNum += 1 + curLen += 1 + } + // 更新最长连续序列的长度 + res = max(res, curLen) + } + + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int longestConsecutive(int[] nums) { + // 转化成哈希集合,方便快速查找是否存在某个元素 + HashSet set = new HashSet(); + for (int num : nums) { + set.add(num); + } + + int res = 0; + + for (int num : set) { + if (set.contains(num - 1)) { + // num 不是连续子序列的第一个,跳过 + continue; + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + int curNum = num; + int curLen = 1; + + while (set.contains(curNum + 1)) { + curNum += 1; + curLen += 1; + } + // 更新最长连续序列的长度 + res = Math.max(res, curLen); + } + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestConsecutive = function(nums) { + // 转化成哈希集合,方便快速查找是否存在某个元素 + let set = new Set(nums); + + let res = 0; + + for (let num of set) { + if (set.has(num - 1)) { + // num 不是连续子序列的第一个,跳过 + continue; + } + // num 是连续子序列的第一个,开始向上计算连续子序列的长度 + let curNum = num; + let curLen = 1; + + while (set.has(curNum + 1)) { + curNum += 1; + curLen += 1; + } + // 更新最长连续序列的长度 + res = Math.max(res, curLen); + } + + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def longestConsecutive(self, nums: List[int]) -> int: + # 转化成哈希集合,方便快速查找是否存在某个元素 + set_num = set(nums) + + res = 0 + + for num in set_num: + if num - 1 in set_num: + # num 不是连续子序列的第一个,跳过 + continue + # num 是连续子序列的第一个,开始向上计算连续子序列的长度 + cur_num = num + cur_len = 1 + + while cur_num + 1 in set_num: + cur_num += 1 + cur_len += 1 + # 更新最长连续序列的长度 + res = max(res, cur_len) + + return res +``` + +https://leetcode.cn/problems/longest-consecutive-sequence 的多语言解法👆 + +https://leetcode.cn/problems/longest-increasing-subsequence 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int lengthOfLIS(vector& nums) { + // dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度 + vector dp(nums.size(), 1); + /** + ![](../pictures/最长递增子序列/5.jpeg) + */ + // base case:dp 数组全都初始化为 1 + + for (int i = 0; i < nums.size(); i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) + dp[i] = max(dp[i], dp[j] + 1); + } + } + + int res = 0; + for (int i = 0; i < dp.size(); i++) { + res = max(res, dp[i]); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func lengthOfLIS(nums []int) int { + // dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度 + dp := make([]int, len(nums)) + // base case:dp 数组全都初始化为 1 + for i := 0; i < len(dp); i++ { + dp[i] = 1 + } + + for i := 0; i < len(nums); i++ { + for j := 0; j < i; j++ { + if nums[i] > nums[j] { + dp[i] = max(dp[i], dp[j]+1) + } + } + } + + res := 0 + for i := 0; i < len(dp); i++ { + res = max(res, dp[i]) + } + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int lengthOfLIS(int[] nums) { + // dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度 + int[] dp = new int[nums.length]; + /** + ![](../pictures/最长递增子序列/5.jpeg) + */ + // base case:dp 数组全都初始化为 1 + Arrays.fill(dp, 1); + + for (int i = 0; i < nums.length; i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + + int res = 0; + for (int i = 0; i < dp.length; i++) { + res = Math.max(res, dp[i]); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var lengthOfLIS = function(nums) { + // dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度 + let dp = new Array(nums.length).fill(1); + /** + ![](../pictures/最长递增子序列/5.jpeg) + */ + // base case:dp 数组全都初始化为 1 + + for (let i = 0; i < nums.length; i++) { + for (let j = 0; j < i; j++) { + if (nums[i] > nums[j]) + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + + let res = 0; + for (let i = 0; i < dp.length; i++) { + res = Math.max(res, dp[i]); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def lengthOfLIS(self, nums: List[int]) -> int: + # dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度 + dp = [1] * len(nums) + + for i in range(len(nums)): + for j in range(i): + if nums[i] > nums[j]: + dp[i] = max(dp[i], dp[j] + 1) + + return max(dp) +``` + +https://leetcode.cn/problems/longest-increasing-subsequence 的多语言解法👆 + +https://leetcode.cn/problems/longest-palindromic-subsequence 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int longestPalindromeSubseq(string s) { + int n = s.length(); + // dp 数组全部初始化为 0 + vector> dp(n, vector(n, 0)); + // base case + for (int i = 0; i < n; i++) { + dp[i][i] = 1; + } + // 反着遍历保证正确的状态转移 + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // 状态转移方程 + if (s[i] == s[j]) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); + } + } + } + // 整个 s 的最长回文子串长度 + return dp[0][n - 1]; + } +}; +``` + +```go +// by chatGPT (go) +func longestPalindromeSubseq(s string) int { + n := len(s) + // dp 数组全部初始化为 0 + dp := make([][]int, n) + for i := range dp { + dp[i] = make([]int, n) + } + + // base case + for i := 0; i < n; i++ { + dp[i][i] = 1 + } + + // 反着遍历保证正确的状态转移 + for i := n - 1; i >= 0; i-- { + for j := i + 1; j < n; j++ { + // 状态转移方程 + if s[i] == s[j] { + dp[i][j] = dp[i+1][j-1] + 2 + } else { + dp[i][j] = max(dp[i+1][j], dp[i][j-1]) + } + } + } + + // 整个 s 的最长回文子串长度 + return dp[0][n-1] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int longestPalindromeSubseq(String s) { + int n = s.length(); + // dp 数组全部初始化为 0 + int[][] dp = new int[n][n]; + // base case + for (int i = 0; i < n; i++) { + dp[i][i] = 1; + } + // 反着遍历保证正确的状态转移 + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // 状态转移方程 + if (s.charAt(i) == s.charAt(j)) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); + } + } + } + // 整个 s 的最长回文子串长度 + return dp[0][n - 1]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestPalindromeSubseq = function(s) { + const n = s.length; + // dp 数组全部初始化为 0 + const dp = new Array(n).fill(0).map(() => new Array(n).fill(0)); + // base case + for (let i = 0; i < n; i++) { + dp[i][i] = 1; + } + // 反着遍历保证正确的状态转移 + for (let i = n - 1; i >= 0; i--) { + for (let j = i + 1; j < n; j++) { + // 状态转移方程 + if (s.charAt(i) === s.charAt(j)) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); + } + } + } + // 整个 s 的最长回文子串长度 + return dp[0][n - 1]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def longestPalindromeSubseq(self, s: str) -> int: + n = len(s) + # dp 数组全部初始化为 0 + dp = [[0 for _ in range(n)] for _ in range(n)] + # base case + for i in range(n): + dp[i][i] = 1 + # 反着遍历保证正确的状态转移 + for i in range(n - 1, -1, -1): + for j in range(i + 1, n): + # 状态转移方程 + if s[i] == s[j]: + dp[i][j] = dp[i + 1][j - 1] + 2 + else: + dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) + # 整个 s 的最长回文子串长度 + return dp[0][n - 1] +``` + +https://leetcode.cn/problems/longest-palindromic-subsequence 的多语言解法👆 + +https://leetcode.cn/problems/longest-palindromic-substring 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string longestPalindrome(string s) { + string res = ""; + for (int i = 0; i < s.length(); i++) { + // 以 s[i] 为中心的最长回文子串 + string s1 = palindrome(s, i, i); + // 以 s[i] 和 s[i+1] 为中心的最长回文子串 + string s2 = palindrome(s, i, i + 1); + // res = longest(res, s1, s2) + res = res.length() > s1.length() ? res : s1; + res = res.length() > s2.length() ? res : s2; + } + return res; + } + + string palindrome(string s, int l, int r) { + // 防止索引越界 + while (l >= 0 && r < s.length() + && s[l] == s[r]) { + // 向两边展开 + l--; + r++; + } + // 返回以 s[l] 和 s[r] 为中心的最长回文串 + return s.substr(l + 1, r - l - 1); + } +}; +``` + +```go +// by chatGPT (go) +func longestPalindrome(s string) string { + res := "" + for i := 0; i < len(s); i++ { + // 以 s[i] 为中心的最长回文子串 + s1 := palindrome(s, i, i) + // 以 s[i] 和 s[i+1] 为中心的最长回文子串 + s2 := palindrome(s, i, i+1) + // res = longest(res, s1, s2) + if len(s1) > len(res) { + res = s1 + } + if len(s2) > len(res) { + res = s2 + } + } + return res +} + +func palindrome(s string, l, r int) string { + // 防止索引越界 + for l >= 0 && r < len(s) && s[l] == s[r] { + // 向两边展开 + l-- + r++ + } + // 返回以 s[l] 和 s[r] 为中心的最长回文串 + return s[l+1 : r] +} +``` + +```java +// by labuladong (java) +class Solution { + public String longestPalindrome(String s) { + String res = ""; + for (int i = 0; i < s.length(); i++) { + // 以 s[i] 为中心的最长回文子串 + String s1 = palindrome(s, i, i); + // 以 s[i] 和 s[i+1] 为中心的最长回文子串 + String s2 = palindrome(s, i, i + 1); + // res = longest(res, s1, s2) + res = res.length() > s1.length() ? res : s1; + res = res.length() > s2.length() ? res : s2; + } + return res; + } + + String palindrome(String s, int l, int r) { + // 防止索引越界 + while (l >= 0 && r < s.length() + && s.charAt(l) == s.charAt(r)) { + // 向两边展开 + l--; + r++; + } + // 返回以 s[l] 和 s[r] 为中心的最长回文串 + return s.substring(l + 1, r); + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestPalindrome = function(s) { + let res = ""; + for (let i = 0; i < s.length; i++) { + // 以 s[i] 为中心的最长回文子串 + let s1 = palindrome(s, i, i); + // 以 s[i] 和 s[i+1] 为中心的最长回文子串 + let s2 = palindrome(s, i, i + 1); + // res = longest(res, s1, s2) + res = res.length > s1.length ? res : s1; + res = res.length > s2.length ? res : s2; + } + return res; + + function palindrome(s, l, r) { + // 防止索引越界 + while (l >= 0 && r < s.length + && s.charAt(l) == s.charAt(r)) { + // 向两边展开 + l--; + r++; + } + // 返回以 s[l] 和 s[r] 为中心的最长回文串 + return s.substring(l + 1, r); + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def longestPalindrome(self, s: str) -> str: + res = "" + for i in range(len(s)): + # 以 s[i] 为中心的最长回文子串 + s1 = self.palindrome(s, i, i) + # 以 s[i] 和 s[i+1] 为中心的最长回文子串 + s2 = self.palindrome(s, i, i + 1) + # res = longest(res, s1, s2) + res = res if len(res) > len(s1) else s1 + res = res if len(res) > len(s2) else s2 + return res + + def palindrome(self, s: str, l: int, r: int) -> str: + # 防止索引越界 + while l >= 0 and r < len(s) and s[l] == s[r]: + # 向两边展开 + l -= 1 + r += 1 + # 返回以 s[l] 和 s[r] 为中心的最长回文串 + return s[l+1:r] +``` + +https://leetcode.cn/problems/longest-palindromic-substring 的多语言解法👆 + +https://leetcode.cn/problems/longest-substring-without-repeating-characters 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + int lengthOfLongestSubstring(string s) { + unordered_map window; + + int left = 0, right = 0; + int res = 0; // 记录结果 + while (right < s.size()) { + char c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + window[c]++; + // 判断左侧窗口是否要收缩 + while (window[c] > 1) { + char d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + window[d]--; + } + // 在这里更新答案 + res = max(res, right - left); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func lengthOfLongestSubstring(s string) int { + window := make(map[byte]int) + + left, right := 0, 0 + res := 0 // 记录结果 + for right < len(s) { + c := s[right] + right++ + // 进行窗口内数据的一系列更新 + window[c]++ + // 判断左侧窗口是否要收缩 + for window[c] > 1 { + d := s[left] + left++ + // 进行窗口内数据的一系列更新 + window[d]-- + } + // 在这里更新答案 + res = max(res, right - left) + } + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by chatGPT (java) +class Solution { + public int lengthOfLongestSubstring(String s) { + Map window = new HashMap<>(); + + int left = 0, right = 0; + int res = 0; // 记录结果 + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + window.put(c, window.getOrDefault(c, 0) + 1); + // 判断左侧窗口是否要收缩 + while (window.get(c) > 1) { + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + window.put(d, window.get(d) - 1); + } + // 在这里更新答案 + res = Math.max(res, right - left); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var lengthOfLongestSubstring = function(s) { + let window = new Map(); + let left = 0, right = 0, res = 0; + while (right < s.length) { + let c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + window.set(c, (window.get(c) || 0) + 1); + // 判断左侧窗口是否要收缩 + while (window.get(c) > 1) { + let d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + window.set(d, window.get(d) - 1); + } + // 在这里更新答案 + res = Math.max(res, right - left); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + window = {} + + left = right = 0 + res = 0 # 记录结果 + while right < len(s): + c = s[right] + right += 1 + # 进行窗口内数据的一系列更新 + window[c] = window.get(c, 0) + 1 + # 判断左侧窗口是否要收缩 + while window[c] > 1: + d = s[left] + left += 1 + # 进行窗口内数据的一系列更新 + window[d] -= 1 + # 在这里更新答案 + res = max(res, right - left) + return res +``` + +https://leetcode.cn/problems/longest-substring-without-repeating-characters 的多语言解法👆 + +https://leetcode.cn/problems/longest-valid-parentheses 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int longestValidParentheses(string s) { + stack stk; + // dp[i] 的定义:记录以 s[i-1] 结尾的最长合法括号子串长度 + vector dp(s.length() + 1, 0); + for (int i = 0; i < s.length(); i++) { + if (s[i] == '(') { + // 遇到左括号,记录索引 + stk.push(i); + // 左括号不可能是合法括号子串的结尾 + dp[i + 1] = 0; + } else { + // 遇到右括号 + if (!stk.empty()) { + // 配对的左括号对应索引 + int leftIndex = stk.top(); + stk.pop(); + // 以这个右括号结尾的最长子串长度 + int len = 1 + i - leftIndex + dp[leftIndex]; + dp[i + 1] = len; + } else { + // 没有配对的左括号 + dp[i + 1] = 0; + } + } + } + // 计算最长子串的长度 + int res = 0; + for (int i = 0; i < dp.size(); i++) { + res = max(res, dp[i]); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func longestValidParentheses(s string) int { + stk := make([]int, 0) + // dp[i] 的定义:记录以 s[i-1] 结尾的最长合法括号子串长度 + dp := make([]int, len(s)+1) + for i := 0; i < len(s); i++ { + if s[i] == '(' { + // 遇到左括号,记录索引 + stk = append(stk, i) + // 左括号不可能是合法括号子串的结尾 + dp[i+1] = 0 + } else { + // 遇到右括号 + if len(stk) != 0 { + // 配对的左括号对应索引 + leftIndex := stk[len(stk)-1] + stk = stk[:len(stk)-1] + // 以这个右括号结尾的最长子串长度 + len := 1 + i - leftIndex + dp[leftIndex] + dp[i+1] = len + } else { + // 没有配对的左括号 + dp[i+1] = 0 + } + } + } + // 计算最长子串的长度 + res := 0 + for i := 0; i < len(dp); i++ { + res = max(res, dp[i]) + } + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int longestValidParentheses(String s) { + Stack stk = new Stack<>(); + // dp[i] 的定义:记录以 s[i-1] 结尾的最长合法括号子串长度 + int[] dp = new int[s.length() + 1]; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + // 遇到左括号,记录索引 + stk.push(i); + // 左括号不可能是合法括号子串的结尾 + dp[i + 1] = 0; + } else { + // 遇到右括号 + if (!stk.isEmpty()) { + // 配对的左括号对应索引 + int leftIndex = stk.pop(); + // 以这个右括号结尾的最长子串长度 + int len = 1 + i - leftIndex + dp[leftIndex]; + dp[i + 1] = len; + } else { + // 没有配对的左括号 + dp[i + 1] = 0; + } + } + } + // 计算最长子串的长度 + int res = 0; + for (int i = 0; i < dp.length; i++) { + res = Math.max(res, dp[i]); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestValidParentheses = function(s) { + let stk = []; + // dp[i] 的定义:记录以 s[i-1] 结尾的最长合法括号子串长度 + let dp = new Array(s.length + 1).fill(0); + for (let i = 0; i < s.length; i++) { + if (s.charAt(i) === '(') { + // 遇到左括号,记录索引 + stk.push(i); + // 左括号不可能是合法括号子串的结尾 + dp[i + 1] = 0; + } else { + // 遇到右括号 + if (stk.length > 0) { + // 配对的左括号对应索引 + let leftIndex = stk.pop(); + // 以这个右括号结尾的最长子串长度 + let len = 1 + i - leftIndex + dp[leftIndex]; + dp[i + 1] = len; + } else { + // 没有配对的左括号 + dp[i + 1] = 0; + } + } + } + // 计算最长子串的长度 + let res = 0; + for (let i = 0; i < dp.length; i++) { + res = Math.max(res, dp[i]); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def longestValidParentheses(self, s: str) -> int: + stk = [] + # dp[i] 的定义:记录以 s[i-1] 结尾的最长合法括号子串长度 + dp = [0] * (len(s) + 1) + for i in range(len(s)): + if s[i] == '(': + # 遇到左括号,记录索引 + stk.append(i) + # 左括号不可能是合法括号子串的结尾 + dp[i + 1] = 0 + else: + # 遇到右括号 + if stk: + # 配对的左括号对应索引 + leftIndex = stk.pop() + # 以这个右括号结尾的最长子串长度 + length = 1 + i - leftIndex + dp[leftIndex] + dp[i + 1] = length + else: + # 没有配对的左括号 + dp[i + 1] = 0 + # 计算最长子串的长度 + res = 0 + for i in range(len(dp)): + res = max(res, dp[i]) + return res +``` + +https://leetcode.cn/problems/longest-valid-parentheses 的多语言解法👆 + +https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + // base case + if (root == nullptr) return nullptr; + if (root == p || root == q) return root; + + TreeNode* left = lowestCommonAncestor(root->left, p, q); + TreeNode* right = lowestCommonAncestor(root->right, p, q); + // 情况 1 + if (left != nullptr && right != nullptr) { + return root; + } + // 情况 2 + if (left == nullptr && right == nullptr) { + return nullptr; + } + // 情况 3 + return left == nullptr ? right : left; + } +}; +``` + +```go +// by chatGPT (go) +func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { + // base case + if root == nil { + return nil + } + if root == p || root == q { + return root + } + + left := lowestCommonAncestor(root.Left, p, q) + right := lowestCommonAncestor(root.Right, p, q) + // 情况 1 + if left != nil && right != nil { + return root + } + // 情况 2 + if left == nil && right == nil { + return nil + } + // 情况 3 + if left == nil { + return right + } + return left +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + // base case + if (root == null) return null; + if (root == p || root == q) return root; + + TreeNode left = lowestCommonAncestor(root.left, p, q); + TreeNode right = lowestCommonAncestor(root.right, p, q); + // 情况 1 + if (left != null && right != null) { + return root; + } + // 情况 2 + if (left == null && right == null) { + return null; + } + // 情况 3 + return left == null ? right : left; + } +} +``` + +```javascript +// by chatGPT (javascript) +var lowestCommonAncestor = function(root, p, q) { + // base case + if (root === null) return null; + if (root === p || root === q) return root; + + var left = lowestCommonAncestor(root.left, p, q); + var right = lowestCommonAncestor(root.right, p, q); + // 情况 1 + if (left !== null && right !== null) { + return root; + } + // 情况 2 + if (left === null && right === null) { + return null; + } + // 情况 3 + return left === null ? right : left; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': + # base case + if not root: + return None + if root == p or root == q: + return root + + left = self.lowestCommonAncestor(root.left, p, q) + right = self.lowestCommonAncestor(root.right, p, q) + # 情况 1 + if left and right: + return root + # 情况 2 + if not left and not right: + return None + # 情况 3 + return right if not left else left +``` + +https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/lru-cache 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class LRUCache { +private: + int cap; + unordered_map cache; + +public: + LRUCache(int capacity) { + this->cap = capacity; + } + + int get(int key) { + if (cache.find(key) == cache.end()) { + return -1; + } + // 将 key 变为最近使用 + makeRecently(key); + return cache[key]; + } + + void put(int key, int val) { + if (cache.find(key) != cache.end()) { + // 修改 key 的值 + cache[key] = val; + // 将 key 变为最近使用 + makeRecently(key); + return; + } + + if (cache.size() >= this->cap) { + // 链表头部就是最久未使用的 key + int oldestKey = cache.begin()->first; + cache.erase(oldestKey); + } + // 将新的 key 添加链表尾部 + cache[key] = val; + } + +private: + void makeRecently(int key) { + int val = cache[key]; + // 删除 key,重新插入到队尾 + cache.erase(key); + cache[key] = val; + } +}; +``` + +```go +// by chatGPT (go) +type LRUCache struct { + cap int + cache map[int]int + list *list.List +} + +func Constructor(capacity int) LRUCache { + return LRUCache{ + cap: capacity, + cache: make(map[int]int), + list: list.New(), + } +} + +func (this *LRUCache) Get(key int) int { + if val, ok := this.cache[key]; ok { + // 将 key 移动到链表尾部表示最近访问 + this.makeRecently(key) + return val + } + return -1 +} + +func (this *LRUCache) Put(key int, value int) { + if _, ok := this.cache[key]; ok { + // 修改 key 的值,将 key 移动到链表尾部表示最近访问 + this.cache[key] = value + this.makeRecently(key) + return + } + + if len(this.cache) >= this.cap { + // 链表头部就是最久未使用的 key + this.removeOldest() + } + // 将新的 key-value 添加链表尾部 + this.cache[key] = value + this.list.PushBack(key) +} + +func (this *LRUCache) makeRecently(key int) { + // 将 key 移动到链表尾部表示最近访问 + for e := this.list.Front(); e != nil; e = e.Next() { + if e.Value.(int) == key { + this.list.MoveToBack(e) + break + } + } +} + +func (this *LRUCache) removeOldest() { + // 删除链表头部表示最久未使用的 key + e := this.list.Front() + delete(this.cache, e.Value.(int)) + this.list.Remove(e) +} +``` + +```java +// by labuladong (java) +class LRUCache { + int cap; + LinkedHashMap cache = new LinkedHashMap<>(); + public LRUCache(int capacity) { + this.cap = capacity; + } + + public int get(int key) { + if (!cache.containsKey(key)) { + return -1; + } + // 将 key 变为最近使用 + makeRecently(key); + return cache.get(key); + } + + public void put(int key, int val) { + if (cache.containsKey(key)) { + // 修改 key 的值 + cache.put(key, val); + // 将 key 变为最近使用 + makeRecently(key); + return; + } + + if (cache.size() >= this.cap) { + // 链表头部就是最久未使用的 key + int oldestKey = cache.keySet().iterator().next(); + cache.remove(oldestKey); + } + // 将新的 key 添加链表尾部 + cache.put(key, val); + } + + private void makeRecently(int key) { + int val = cache.get(key); + // 删除 key,重新插入到队尾 + cache.remove(key); + cache.put(key, val); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} capacity + */ +var LRUCache = function(capacity) { + this.cap = capacity; + this.cache = new Map(); +}; + +/** + * @param {number} key + * @return {number} + */ +LRUCache.prototype.get = function(key) { + if (!this.cache.has(key)) { + return -1; + } + // 将 key 变为最近使用 + this.makeRecently(key); + return this.cache.get(key); +}; + +/** + * @param {number} key + * @param {number} value + * @return {void} + */ +LRUCache.prototype.put = function(key, value) { + if (this.cache.has(key)) { + // 修改 key 的值 + this.cache.set(key, value); + // 将 key 变为最近使用 + this.makeRecently(key); + return; + } + + if (this.cache.size >= this.cap) { + // 链表头部就是最久未使用的 key + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + // 将新的 key 添加链表尾部 + this.cache.set(key, value); +}; + +/** + * @param {number} key + * @return {void} + */ +LRUCache.prototype.makeRecently = function(key) { + const val = this.cache.get(key); + // 删除 key,重新插入到队尾 + this.cache.delete(key); + this.cache.set(key, val); +}; +``` + +```python +# by chatGPT (python) +class LRUCache: + def __init__(self, capacity: int): + self.cap = capacity + self.cache = collections.OrderedDict() + + def get(self, key: int) -> int: + if key not in self.cache: + return -1 + # 将 key 变为最近使用 + self.cache.move_to_end(key) + return self.cache[key] + + def put(self, key: int, value: int) -> None: + if key in self.cache: + # 修改 key 的值 + self.cache[key] = value + # 将 key 变为最近使用 + self.cache.move_to_end(key) + return + + if len(self.cache) >= self.cap: + # 链表头部就是最久未使用的 key + self.cache.popitem(last=False) + # 将新的 key 添加链表尾部 + self.cache[key] = value +``` + +https://leetcode.cn/problems/lru-cache 的多语言解法👆 + +https://leetcode.cn/problems/majority-element 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int majorityElement(vector& nums) { + // 我们想寻找的那个众数 + int target = 0; + // 计数器(类比带电粒子例子中的带电性) + int count = 0; + for (int i = 0; i < nums.size(); i++) { + if (count == 0) { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i]; + // 众数出现了一次 + count = 1; + } else if (nums[i] == target) { + // 如果遇到的是目标众数,计数器累加 + count++; + } else { + // 如果遇到的不是目标众数,计数器递减 + count--; + } + } + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target; + } +}; +``` + +```go +// by chatGPT (go) +func majorityElement(nums []int) int { + // 我们想寻找的那个众数 + target := 0 + // 计数器(类比带电粒子例子中的带电性) + count := 0 + for i := 0; i < len(nums); i++ { + if count == 0 { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i] + // 众数出现了一次 + count = 1 + } else if nums[i] == target { + // 如果遇到的是目标众数,计数器累加 + count++ + } else { + // 如果遇到的不是目标众数,计数器递减 + count-- + } + } + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target +} +``` + +```java +// by labuladong (java) +class Solution { + public int majorityElement(int[] nums) { + // 我们想寻找的那个众数 + int target = 0; + // 计数器(类比带电粒子例子中的带电性) + int count = 0; + for (int i = 0; i < nums.length; i++) { + if (count == 0) { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i]; + // 众数出现了一次 + count = 1; + } else if (nums[i] == target) { + // 如果遇到的是目标众数,计数器累加 + count++; + } else { + // 如果遇到的不是目标众数,计数器递减 + count--; + } + } + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target; + } +} +``` + +```javascript +// by chatGPT (javascript) +var majorityElement = function(nums) { + // 我们想寻找的那个众数 + let target = 0; + // 计数器(类比带电粒子例子中的带电性) + let count = 0; + for (let i = 0; i < nums.length; i++) { + if (count === 0) { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i]; + // 众数出现了一次 + count = 1; + } else if (nums[i] === target) { + // 如果遇到的是目标众数,计数器累加 + count++; + } else { + // 如果遇到的不是目标众数,计数器递减 + count--; + } + } + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def majorityElement(self, nums: List[int]) -> int: + # 我们想寻找的那个众数 + target = 0 + # 计数器(类比带电粒子例子中的带电性) + count = 0 + for i in range(len(nums)): + if count == 0: + # 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i] + # 众数出现了一次 + count = 1 + elif nums[i] == target: + # 如果遇到的是目标众数,计数器累加 + count += 1 + else: + # 如果遇到的不是目标众数,计数器递减 + count -= 1 + # 回想带电粒子的例子 + # 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target +``` + +https://leetcode.cn/problems/majority-element 的多语言解法👆 + +https://leetcode.cn/problems/matrix-block-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class NumMatrix { +private: + vector> preSum; +public: + NumMatrix(vector>& matrix) { + int m = matrix.size(), n = matrix[0].size(); + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = vector>(m + 1, vector(n + 1)); + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] + matrix[i - 1][j - 1] - preSum[i - 1][j - 1]; + } + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + int sumRegion(int x1, int y1, int x2, int y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2 + 1][y2 + 1] - preSum[x1][y2 + 1] - preSum[x2 + 1][y1] + preSum[x1][y1]; + } +}; + +class Solution { +public: + vector> matrixBlockSum(vector>& mat, int k) { + int m = mat.size(), n = mat[0].size(); + NumMatrix numMatrix(mat); + vector> res(m, vector(n)); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 左上角的坐标 + int x1 = max(i - k, 0); + int y1 = max(j - k, 0); + // 右下角坐标 + int x2 = min(i + k, m - 1); + int y2 = min(j + k, n - 1); + res[i][j] = numMatrix.sumRegion(x1, y1, x2, y2); + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +type NumMatrix struct { + // 定义:preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和 + preSum [][]int +} + +func Constructor(matrix [][]int) NumMatrix { + m, n := len(matrix), len(matrix[0]) + // 构造前缀和矩阵 + preSum := make([][]int, m+1) + for i := range preSum { + preSum[i] = make([]int, n+1) + } + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i-1][j-1] - preSum[i-1][j-1] + } + } + return NumMatrix{preSum: preSum} +} + +// 计算子矩阵 [x1, y1, x2, y2] 的元素和 +func (n *NumMatrix) SumRegion(x1 int, y1 int, x2 int, y2 int) int { + // 目标矩阵之和由四个相邻矩阵运算获得 + return n.preSum[x2+1][y2+1] - n.preSum[x1][y2+1] - n.preSum[x2+1][y1] + n.preSum[x1][y1] +} + +func matrixBlockSum(mat [][]int, k int) [][]int { + m, n := len(mat), len(mat[0]) + numMatrix := Constructor(mat) + res := make([][]int, m) + for i := range res { + res[i] = make([]int, n) + } + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + // 左上角的坐标 + x1 := max(i-k, 0) + y1 := max(j-k, 0) + // 右下角坐标 + x2 := min(i+k, m-1) + y2 := min(j+k, n-1) + + res[i][j] = numMatrix.SumRegion(x1, y1, x2, y2) + } + } + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] matrixBlockSum(int[][] mat, int k) { + int m = mat.length, n = mat[0].length; + NumMatrix numMatrix = new NumMatrix(mat); + int[][] res = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 左上角的坐标 + int x1 = Math.max(i - k, 0); + int y1 = Math.max(j - k, 0); + // 右下角坐标 + int x2 = Math.min(i + k, m - 1); + int y2 = Math.min(j + k, n - 1); + + res[i][j] = numMatrix.sumRegion(x1, y1, x2, y2); + } + } + return res; + } +} + +class NumMatrix { + // 定义:preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和 + private int[][] preSum; + + public NumMatrix(int[][] matrix) { + int m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] + matrix[i - 1][j - 1] - preSum[i - 1][j - 1]; + } + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + public int sumRegion(int x1, int y1, int x2, int y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2 + 1][y2 + 1] - preSum[x1][y2 + 1] - preSum[x2 + 1][y1] + preSum[x1][y1]; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} mat + * @param {number} k + * @return {number[][]} + */ +var matrixBlockSum = function(mat, k) { + const m = mat.length, n = mat[0].length; + const numMatrix = new NumMatrix(mat); + const res = new Array(m); + for (let i = 0; i < m; i++) { + res[i] = new Array(n); + for (let j = 0; j < n; j++) { + // 左上角的坐标 + const x1 = Math.max(i - k, 0); + const y1 = Math.max(j - k, 0); + // 右下角坐标 + const x2 = Math.min(i + k, m - 1); + const y2 = Math.min(j + k, n - 1); + + res[i][j] = numMatrix.sumRegion(x1, y1, x2, y2); + } + } + return res; +}; + +class NumMatrix { + /** + * @param {number[][]} matrix + */ + constructor(matrix) { + const m = matrix.length, n = matrix[0].length; + if (m === 0 || n === 0) return; + // 构造前缀和矩阵 + this.preSum = new Array(m + 1); + for (let i = 0; i <= m; i++) { + this.preSum[i] = new Array(n + 1).fill(0); + } + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + this.preSum[i][j] = this.preSum[i - 1][j] + this.preSum[i][j - 1] + matrix[i - 1][j - 1] - this.preSum[i - 1][j - 1]; + } + } + } + + /** + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @return {number} + */ + sumRegion(x1, y1, x2, y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return this.preSum[x2 + 1][y2 + 1] - this.preSum[x1][y2 + 1] - this.preSum[x2 + 1][y1] + this.preSum[x1][y1]; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def matrixBlockSum(self, mat: List[List[int]], k: int) -> List[List[int]]: + m, n = len(mat), len(mat[0]) + numMatrix = NumMatrix(mat) + res = [[0]*n for _ in range(m)] + for i in range(m): + for j in range(n): + # 左上角的坐标 + x1, y1 = max(i - k, 0), max(j - k, 0) + # 右下角坐标 + x2, y2 = min(i + k, m - 1), min(j + k, n - 1) + + res[i][j] = numMatrix.sumRegion(x1, y1, x2, y2) + return res + +class NumMatrix: + def __init__(self, matrix: List[List[int]]): + m, n = len(matrix), len(matrix[0]) + if m == 0 or n == 0: + return + # 构造前缀和矩阵 + self.preSum = [[0]*(n+1) for _ in range(m+1)] + for i in range(1, m+1): + for j in range(1, n+1): + # 计算每个矩阵 [0, 0, i, j] 的元素和 + self.preSum[i][j] = self.preSum[i-1][j] + self.preSum[i][j-1] + matrix[i-1][j-1] - self.preSum[i-1][j-1] + + # 计算子矩阵 [x1, y1, x2, y2] 的元素和 + def sumRegion(self, x1: int, y1: int, x2: int, y2: int) -> int: + # 目标矩阵之和由四个相邻矩阵运算获得 + return self.preSum[x2+1][y2+1] - self.preSum[x1][y2+1] - self.preSum[x2+1][y1] + self.preSum[x1][y1] +``` + +https://leetcode.cn/problems/matrix-block-sum 的多语言解法👆 + +https://leetcode.cn/problems/max-area-of-island 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxAreaOfIsland(vector>& grid) { + // 记录岛屿的最大面积 + int res = 0; + int m = grid.size(), n = grid[0].size(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹没岛屿,并更新最大岛屿面积 + res = max(res, dfs(grid, i, j)); + } + } + } + return res; + } + + // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + int dfs(vector>& grid, int i, int j) { + int m = grid.size(), n = grid[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] == 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; + + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + } +}; +``` + +```go +// by chatGPT (go) +func maxAreaOfIsland(grid [][]int) int { + // 记录岛屿的最大面积 + res := 0 + m, n := len(grid), len(grid[0]) + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid[i][j] == 1 { + // 淹没岛屿,并更新最大岛屿面积 + res = Max(res, dfs(grid, i, j)) + } + } + } + return res +} + +// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 +func dfs(grid [][]int, i int, j int) int { + m, n := len(grid), len(grid[0]) + if i < 0 || j < 0 || i >= m || j >= n { + // 超出索引边界 + return 0 + } + if grid[i][j] == 0 { + // 已经是海水了 + return 0 + } + // 将 (i, j) 变成海水 + grid[i][j] = 0 + + return dfs(grid, i+1, j) + + dfs(grid, i, j+1) + + dfs(grid, i-1, j) + + dfs(grid, i, j-1) + 1 +} + +// Max 获取两个数的最大值 +func Max(x int, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxAreaOfIsland(int[][] grid) { + // 记录岛屿的最大面积 + int res = 0; + int m = grid.length, n = grid[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹没岛屿,并更新最大岛屿面积 + res = Math.max(res, dfs(grid, i, j)); + } + } + } + return res; + } + + // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + int dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] == 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; + + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxAreaOfIsland = function(grid) { + // 记录岛屿的最大面积 + let res = 0; + const m = grid.length, n = grid[0].length; + + // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + const dfs = (grid, i, j) => { + const m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] === 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; + + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + }; + + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (grid[i][j] === 1) { + // 淹没岛屿,并更新最大岛屿面积 + res = Math.max(res, dfs(grid, i, j)); + } + } + } + + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxAreaOfIsland(self, grid: List[List[int]]) -> int: + # 记录岛屿的最大面积 + res = 0 + m, n = len(grid), len(grid[0]) + for i in range(m): + for j in range(n): + if grid[i][j] == 1: + # 淹没岛屿,并更新最大岛屿面积 + res = max(res, self.dfs(grid, i, j)) + return res + + # 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + def dfs(self, grid: List[List[int]], i: int, j: int) -> int: + m, n = len(grid), len(grid[0]) + if i < 0 or j < 0 or i >= m or j >= n: + # 超出索引边界 + return 0 + if grid[i][j] == 0: + # 已经是海水了 + return 0 + # 将 (i, j) 变成海水 + grid[i][j] = 0 + + return self.dfs(grid, i + 1, j) \ + + self.dfs(grid, i - 1, j) \ + + self.dfs(grid, i, j + 1) \ + + self.dfs(grid, i, j - 1) \ + + 1 +``` + +https://leetcode.cn/problems/max-area-of-island 的多语言解法👆 + +https://leetcode.cn/problems/maximum-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + /* 主函数 */ + TreeNode* constructMaximumBinaryTree(vector& nums) { + return build(nums, 0, nums.size() - 1); + } + + /* 定义:将 nums[lo..hi] 构造成符合条件的树,返回根节点 */ + TreeNode* build(vector& nums, int lo, int hi) { + /* base case */ + if (lo > hi) { + return nullptr; + } + + /* 找到数组中的最大值和对应的索引 */ + int index = -1, maxVal = INT_MIN; + for (int i = lo; i <= hi; i++) { + if (maxVal < nums[i]) { + index = i; + maxVal = nums[i]; + } + } + + TreeNode* root = new TreeNode(maxVal); + /* 递归调用构造左右子树 */ + root->left = build(nums, lo, index - 1); + root->right = build(nums, index + 1, hi); + + return root; + } +}; +``` + +```go +// by chatGPT (go) +func constructMaximumBinaryTree(nums []int) *TreeNode { + return build(nums, 0, len(nums)-1) +} + +func build(nums []int, lo int, hi int) *TreeNode { + // base case + if lo > hi { + return nil + } + + // 找到数组中的最大值和对应的索引 + index, maxVal := -1, math.MinInt32 + for i := lo; i <= hi; i++ { + if maxVal < nums[i] { + index = i + maxVal = nums[i] + } + } + + root := &TreeNode{Val: maxVal} + // 递归调用构造左右子树 + root.Left = build(nums, lo, index-1) + root.Right = build(nums, index+1, hi) + + return root +} +``` + +```java +// by labuladong (java) +class Solution { + /* 主函数 */ + public TreeNode constructMaximumBinaryTree(int[] nums) { + return build(nums, 0, nums.length - 1); + } + + /* 定义:将 nums[lo..hi] 构造成符合条件的树,返回根节点 */ + TreeNode build(int[] nums, int lo, int hi) { + // base case + if (lo > hi) { + return null; + } + + // 找到数组中的最大值和对应的索引 + int index = -1, maxVal = Integer.MIN_VALUE; + for (int i = lo; i <= hi; i++) { + if (maxVal < nums[i]) { + index = i; + maxVal = nums[i]; + } + } + + TreeNode root = new TreeNode(maxVal); + // 递归调用构造左右子树 + root.left = build(nums, lo, index - 1); + root.right = build(nums, index + 1, hi); + + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {TreeNode} + */ +var constructMaximumBinaryTree = function(nums) { + return build(nums, 0, nums.length - 1); +} + +/** + * Define: 将 nums[lo..hi] 构造成符合条件的树,返回根节点 + */ +var build = function(nums, lo, hi) { + // base case + if (lo > hi) { + return null; + } + + // 找到数组中的最大值和对应的索引 + var index = -1, maxVal = -Infinity; + for (var i = lo; i <= hi; i++) { + if (maxVal < nums[i]) { + index = i; + maxVal = nums[i]; + } + } + + var root = new TreeNode(maxVal); + // 递归调用构造左右子树 + root.left = build(nums, lo, index - 1); + root.right = build(nums, index + 1, hi); + + return root; +} +``` + +```python +# by chatGPT (python) +# Definition for a binary tree node. +# class TreeNode: +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right + +class Solution: + def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode: + """ + :type nums: List[int] + :rtype: TreeNode + """ + def build(nums, lo, hi): + # base case + if lo > hi: + return None + + # 找到数组中的最大值和对应的索引 + index, maxVal = lo, nums[lo] + for i in range(lo, hi+1): + if nums[i] > maxVal: + index, maxVal = i, nums[i] + + # 创建根节点 + root = TreeNode(maxVal) + # 递归构建左右子树 + root.left = build(nums, lo, index - 1) + root.right = build(nums, index + 1, hi) + + return root + + # 调用 build 函数 + return build(nums, 0, len(nums)-1) +``` + +https://leetcode.cn/problems/maximum-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/maximum-depth-of-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +/***** 解法一,回溯算法思路 *****/ +class Solution { +public: + int depth = 0; + int res = 0; + + int maxDepth(TreeNode* root) { + traverse(root); + return res; + } + + // 遍历二叉树 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + + // 前序遍历位置 + depth++; + // 遍历的过程中记录最大深度 + res = max(res, depth); + traverse(root->left); + traverse(root->right); + // 后序遍历位置 + depth--; + } +}; + +/***** 解法二,动态规划思路 *****/ +class Solution2 { +public: + // 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 + int maxDepth(TreeNode* root) { + if (root == nullptr) { + return 0; + } + int leftMax = maxDepth(root->left); + int rightMax = maxDepth(root->right); + // 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + max(leftMax, rightMax); + } +}; +``` + +```go +// by chatGPT (go) +/***** 解法一,回溯算法思路 *****/ +func maxDepth(root *TreeNode) int { + var depth int + var res int + traverse(root, &depth, &res) + return res +} + +// 遍历二叉树 +func traverse(root *TreeNode, depth *int, res *int) { + if root == nil { + return + } + // 前序遍历位置 + *depth++ + // 遍历的过程中记录最大深度 + *res = max(*res,*depth) + traverse(root.Left, depth, res) + traverse(root.Right, depth, res) + // 后序遍历位置 + *depth-- +} + +/***** 解法二,动态规划思路 *****/ +func maxDepth2(root *TreeNode) int { + if root == nil { + return 0 + } + leftMax := maxDepth2(root.Left) + rightMax := maxDepth2(root.Right) + // 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + max(leftMax, rightMax) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +/***** 解法一,回溯算法思路 *****/ +class Solution { + + int depth = 0; + int res = 0; + + public int maxDepth(TreeNode root) { + traverse(root); + return res; + } + + // 遍历二叉树 + void traverse(TreeNode root) { + if (root == null) { + return; + } + + // 前序遍历位置 + depth++; + // 遍历的过程中记录最大深度 + res = Math.max(res, depth); + traverse(root.left); + traverse(root.right); + // 后序遍历位置 + depth--; + } +} + +/***** 解法二,动态规划思路 *****/ +class Solution2 { + // 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 + public int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + // 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + Math.max(leftMax, rightMax); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 解法一,回溯算法思路 + */ +function maxDepth(root) { + let depth = 0; + let res = 0; + // 遍历二叉树 + function traverse(root) { + if (root === null) { + return; + } + + // 前序遍历位置 + depth++; + // 遍历的过程中记录最大深度 + res = Math.max(res, depth); + traverse(root.left); + traverse(root.right); + // 后序遍历位置 + depth--; + } + traverse(root); + return res; +} + +/** + * 解法二,动态规划思路 + */ + +// 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 +function maxDepth2(root) { + if (root === null) { + return 0; + } + const leftMax = maxDepth2(root.left); + const rightMax = maxDepth2(root.right); + // 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + Math.max(leftMax, rightMax); +} +``` + +```python +# by chatGPT (python) +# 解法一,回溯算法思路 + +class Solution: + def maxDepth(self, root: TreeNode) -> int: + self.res = 0 + self.traverse(root, 0) + return self.res + + # 遍历二叉树 + def traverse(self, root: TreeNode, depth: int) -> None: + if not root: + return + # 前序遍历位置 + depth += 1 + # 遍历的过程中记录最大深度 + self.res = max(self.res, depth) + self.traverse(root.left, depth) + self.traverse(root.right, depth) + # 后序遍历位置 + depth -= 1 + + +# 解法二,动态规划思路 + +class Solution: + # 定义:输入一个节点,返回以该节点为根的二叉树的最大深度 + def maxDepth(self, root: TreeNode) -> int: + if not root: + return 0 + leftMax = self.maxDepth(root.left) + rightMax = self.maxDepth(root.right) + # 根据左右子树的最大深度推出原二叉树的最大深度 + return 1 + max(leftMax, rightMax) +``` + +https://leetcode.cn/problems/maximum-depth-of-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/maximum-depth-of-n-ary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 分解问题的思路 +class Solution { +public: + int maxDepth(Node* root) { + if (root == nullptr) { + return 0; + } + int subTreeMaxDepth = 0; + for (Node* child : root->children) { + subTreeMaxDepth = max(subTreeMaxDepth, maxDepth(child)); + } + return 1 + subTreeMaxDepth; + } +}; + +// 遍历的思路 +class Solution2 { +public: + int maxDepth(Node* root) { + traverse(root); + return res; + } + +private: + // 记录递归遍历到的深度 + int depth = 0; + // 记录最大的深度 + int res = 0; + + void traverse(Node* root) { + if (root == nullptr) { + return; + } + // 前序遍历位置 + depth++; + res = max(res, depth); + + for (Node* child : root->children) { + traverse(child); + } + // 后序遍历位置 + depth--; + } +}; +``` + +```go +// by chatGPT (go) +// 分解问题的思路 +func maxDepth(root *Node) int { + if root == nil { + return 0 + } + subTreeMaxDepth := 0 + for _, child := range root.Children { + subTreeMaxDepth = max(subTreeMaxDepth, maxDepth(child)) + } + return 1 + subTreeMaxDepth +} + +// 遍历的思路 +func maxDepth(root *Node) int { + // 记录递归遍历到的深度 + depth := 0 + // 记录最大的深度 + res := 0 + + traverse(root, &depth, &res) + return res +} + +func traverse(root *Node, depth *int, res *int) { + if root == nil { + return + } + // 前序遍历位置 + (*depth)++ + *res = max(*res, *depth) + + for _, child := range root.Children { + traverse(child, depth, res) + } + // 后序遍历位置 + (*depth)-- +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +// 分解问题的思路 +class Solution { + public int maxDepth(Node root) { + if (root == null) { + return 0; + } + int subTreeMaxDepth = 0; + for (Node child : root.children) { + subTreeMaxDepth = Math.max(subTreeMaxDepth, maxDepth(child)); + } + return 1 + subTreeMaxDepth; + } +} + +// 遍历的思路 +class Solution2 { + public int maxDepth(Node root) { + traverse(root); + return res; + } + + // 记录递归遍历到的深度 + int depth = 0; + // 记录最大的深度 + int res = 0; + + void traverse(Node root) { + if (root == null) { + return; + } + // 前序遍历位置 + depth++; + res = Math.max(res, depth); + + for (Node child : root.children) { + traverse(child); + } + // 后序遍历位置 + depth--; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 分解问题的思路 + */ +var maxDepth = function(root) { + // 若根结点为空,则直接返回 0 + if (!root) { + return 0; + } + // 设定子树最大深度 + let subTreeMaxDepth = 0; + // 遍历每个子结点 + for (let i = 0; i < root.children.length; i++) { + subTreeMaxDepth = Math.max(subTreeMaxDepth, maxDepth(root.children[i])); + } + // 当前结点深度为最大子结点深度+1 + return 1 + subTreeMaxDepth; +}; + +/** + * 遍历的思路 + */ +var maxDepth2 = function(root) { + // 记录递归遍历到的深度 + let depth = 0; + // 记录最大的深度 + let res = 0; + + /** + * 递归遍历 + */ + function traverse(root) { + if (!root) { + return; + } + // 前序遍历位置 + depth++; + res = Math.max(res, depth); + + // 遍历每个子节点 + for (let i = 0; i < root.children.length; i++) { + traverse(root.children[i]); + } + // 后序遍历位置 + depth--; + } + + // 递归遍历获取最大深度 + traverse(root); + return res; +} +``` + +```python +# by chatGPT (python) +# 分解问题的思路 +class Solution: + def maxDepth(self, root: 'Node') -> int: + if not root: + return 0 + subTreeMaxDepth = 0 + for child in root.children: + subTreeMaxDepth = max(subTreeMaxDepth, self.maxDepth(child)) + return 1 + subTreeMaxDepth + +# 遍历的思路 +class Solution2: + def maxDepth(self, root: 'Node') -> int: + self.traverse(root) + return self.res + + # 记录递归遍历到的深度 + def __init__(self): + self.depth = 0 + # 记录最大的深度 + self.res = 0 + + def traverse(self, root): + if not root: + return + # 前序遍历位置 + self.depth += 1 + self.res = max(self.res, self.depth) + + for child in root.children: + self.traverse(child) + # 后序遍历位置 + self.depth -= 1 +``` + +https://leetcode.cn/problems/maximum-depth-of-n-ary-tree 的多语言解法👆 + +https://leetcode.cn/problems/maximum-frequency-stack 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class FreqStack { + // 记录 FreqStack 中元素的最大频率 + int maxFreq = 0; + // 记录 FreqStack 中每个 val 对应的出现频率,后文就称为 VF 表 + unordered_map valToFreq; + // 记录频率 freq 对应的 val 列表,后文就称为 FV 表 + unordered_map> freqToVals; + +public: + void push(int val) { + // 修改 VF 表:val 对应的 freq 加一 + int freq = valToFreq[val] + 1; + valToFreq[val] = freq; + // 修改 FV 表:在 freq 对应的列表加上 val + freqToVals[freq].push(val); + // 更新 maxFreq + maxFreq = max(maxFreq, freq); + } + + int pop() { + // 修改 FV 表:pop 出一个 maxFreq 对应的元素 v + stack &vals = freqToVals[maxFreq]; + int v = vals.top(); vals.pop(); + // 修改 VF 表:v 对应的 freq 减一 + int freq = --valToFreq[v]; + // 更新 maxFreq + if (vals.empty()) { + // 如果 maxFreq 对应的元素空了 + maxFreq--; + } + return v; + } +}; +``` + +```go +// by chatGPT (go) +type FreqStack struct { + // 记录 FreqStack 中元素的最大频率 + maxFreq int + // 记录 FreqStack 中每个 val 对应的出现频率,后文就称为 VF 表 + valToFreq map[int]int + // 记录频率 freq 对应的 val 列表,后文就称为 FV 表 + freqToVals map[int]*stack.Stack +} + +func Constructor() FreqStack { + return FreqStack{0, make(map[int]int), make(map[int]*stack.Stack)} +} + +func (this *FreqStack) Push(val int) { + // 修改 VF 表:val 对应的 freq 加一 + freq := this.valToFreq[val] + 1 + this.valToFreq[val] = freq + // 修改 FV 表:在 freq 对应的列表加上 val + if this.freqToVals[freq] == nil { + this.freqToVals[freq] = stack.New() + } + this.freqToVals[freq].Push(val) + // 更新 maxFreq + if freq > this.maxFreq { + this.maxFreq = freq + } +} + +func (this *FreqStack) Pop() int { + // 修改 FV 表:pop 出一个 maxFreq 对应的元素 v + vals := this.freqToVals[this.maxFreq] + v := vals.Pop().(int) + // 修改 VF 表:v 对应的 freq 减一 + freq := this.valToFreq[v] - 1 + this.valToFreq[v] = freq + // 更新 maxFreq + if vals.Len() == 0 { + // 如果 maxFreq 对应的元素空了 + this.maxFreq-- + } + return v +} +``` + +```java +// by labuladong (java) +class FreqStack { + // 记录 FreqStack 中元素的最大频率 + int maxFreq = 0; + // 记录 FreqStack 中每个 val 对应的出现频率,后文就称为 VF 表 + HashMap valToFreq = new HashMap<>(); + // 记录频率 freq 对应的 val 列表,后文就称为 FV 表 + HashMap> freqToVals = new HashMap<>(); + + public void push(int val) { + // 修改 VF 表:val 对应的 freq 加一 + int freq = valToFreq.getOrDefault(val, 0) + 1; + valToFreq.put(val, freq); + // 修改 FV 表:在 freq 对应的列表加上 val + freqToVals.putIfAbsent(freq, new Stack<>()); + freqToVals.get(freq).push(val); + // 更新 maxFreq + maxFreq = Math.max(maxFreq, freq); + } + + public int pop() { + // 修改 FV 表:pop 出一个 maxFreq 对应的元素 v + Stack vals = freqToVals.get(maxFreq); + int v = vals.pop(); + // 修改 VF 表:v 对应的 freq 减一 + int freq = valToFreq.get(v) - 1; + valToFreq.put(v, freq); + // 更新 maxFreq + if (vals.isEmpty()) { + // 如果 maxFreq 对应的元素空了 + maxFreq--; + } + return v; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} val + * @return {void} + */ +var FreqStack = function() { + // 记录 FreqStack 中元素的最大频率 + this.maxFreq = 0; + // 记录 FreqStack 中每个 val 对应的出现频率,后文就称为 VF 表 + this.valToFreq = new Map(); + // 记录频率 freq 对应的 val 列表,后文就称为 FV 表 + this.freqToVals = new Map(); +}; + +/** + * @param {number} val + * @return {void} + */ +FreqStack.prototype.push = function(val) { + // 修改 VF 表:val 对应的 freq 加一 + const freq = this.valToFreq.get(val) + 1 || 1; + this.valToFreq.set(val, freq); + // 修改 FV 表:在 freq 对应的列表加上 val + this.freqToVals.set(freq, this.freqToVals.get(freq) || []); + this.freqToVals.get(freq).push(val); + // 更新 maxFreq + this.maxFreq = Math.max(this.maxFreq, freq); +}; + +/** + * @return {number} + */ +FreqStack.prototype.pop = function() { + // 修改 FV 表:pop 出一个 maxFreq 对应的元素 v + const vals = this.freqToVals.get(this.maxFreq); + const v = vals.pop(); + // 修改 VF 表:v 对应的 freq 减一 + const freq = this.valToFreq.get(v) - 1; + this.valToFreq.set(v, freq); + // 更新 maxFreq + if (vals.length === 0) { + // 如果 maxFreq 对应的元素空了 + this.maxFreq--; + } + return v; +}; +``` + +```python +# by chatGPT (python) +class FreqStack: + def __init__(self): + # 记录 FreqStack 中元素的最大频率 + self.maxFreq = 0 + # 记录 FreqStack 中每个 val 对应的出现频率,后文就称为 VF 表 + self.valToFreq = {} + # 记录频率 freq 对应的 val 列表,后文就称为 FV 表 + self.freqToVals = {} + + def push(self, val: int) -> None: + # 修改 VF 表:val 对应的 freq 加一 + freq = self.valToFreq.get(val, 0) + 1 + self.valToFreq[val] = freq + # 修改 FV 表:在 freq 对应的列表加上 val + self.freqToVals.setdefault(freq, []) + self.freqToVals[freq].append(val) + # 更新 maxFreq + self.maxFreq = max(self.maxFreq, freq) + + def pop(self) -> int: + # 修改 FV 表:pop 出一个 maxFreq 对应的元素 v + vals = self.freqToVals[self.maxFreq] + v = vals.pop() + # 修改 VF 表:v 对应的 freq 减一 + freq = self.valToFreq[v] - 1 + self.valToFreq[v] = freq + # 更新 maxFreq + if not vals: + # 如果 maxFreq 对应的元素空了 + self.maxFreq -= 1 + return v +``` + +https://leetcode.cn/problems/maximum-frequency-stack 的多语言解法👆 + +https://leetcode.cn/problems/maximum-size-subarray-sum-equals-k 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxSubArrayLen(vector& nums, int k) { + int n = nums.size(); + // preSum 中的值 -> 对应的最小索引 + // 比如 preSum = [2,4,1,3,4],preSumToIndex[4] = 1 + unordered_map preSumToIndex; + int maxLen = 0; + // 前缀和数组 + int preSum = 0; + // base case,这样索引相减的时候可以算出正确的子数组长度 + preSumToIndex[0] = -1; + for (int i = 0; i < n; i++) { + // 计算前缀和,维护 preSum = sum(nums[0..i]) + preSum += nums[i]; + // 确保 preSumToIndex 中记录的索引是第一次出现的位置 + preSumToIndex.emplace(preSum, i); + int need = preSum - k; + if (preSumToIndex.contains(need)) { + int j = preSumToIndex[need]; + // nums[j + 1..i] 是和为 k 的子数组 + maxLen = max(maxLen, i - j); + } + } + return maxLen; + } +}; +``` + +```go +// by chatGPT (go) +// +// MaxSubArrayLen returns the maximum length of a contiguous subarray with sum equal to k. +func MaxSubArrayLen(nums []int, k int) int { + n := len(nums) + // preSum 中的值 -> 对应的最小索引 + // 比如 preSum = [2,4,1,3,4],preSumToIndex[4] = 1 + preSumToIndex := make(map[int]int, n) + maxLen := 0 + // 前缀和数组(在这道题中可以优化为一个变量) + preSum := 0 + // base case,这样索引相减的时候可以算出正确的子数组长度 + preSumToIndex[0] = -1 + for i := 0; i < n; i++ { + // 计算前缀和,维护 preSum = sum(nums[0..i]) + preSum += nums[i] + // 确保 preSumToIndex 中记录的索引是第一次出现的位置 + if _, ok := preSumToIndex[preSum]; !ok { + preSumToIndex[preSum] = i + } + need := preSum - k + if j, ok := preSumToIndex[need]; ok { + // nums[j..i] 是和为 k 的子数组 + maxLen = max(maxLen, i-j) + } + } + return maxLen +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public static int maxSubArrayLen(int[] nums, int k) { + int n = nums.length; + // preSum 中的值 -> 对应的最小索引 + // 比如 preSum = [2,4,1,3,4],preSumToIndex[4] = 1 + HashMap preSumToIndex = new HashMap<>(); + int maxLen = 0; + // 前缀和数组(在这道题中可以优化为一个变量) + int preSum = 0; + // base case,这样索引相减的时候可以算出正确的子数组长度 + preSumToIndex.put(0, -1); + for (int i = 0; i < n; i++) { + // 计算前缀和,维护 preSum = sum(nums[0..i]) + preSum += nums[i]; + // 确保 preSumToIndex 中记录的索引是第一次出现的位置 + preSumToIndex.putIfAbsent(preSum, i); + int need = preSum[i] - k; + if (preSumToIndex.containsKey(need)) { + int j = preSumToIndex.get(need); + // nums[j..i] 是和为 k 的子数组 + maxLen = Math.max(maxLen, i - j); + } + } + return maxLen; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @param {number} k + * @return {number} + */ +var maxSubArrayLen = function(nums, k) { + let n = nums.length; + // preSum 中的值 -> 对应的最小索引 + // 比如 preSum = [2,4,1,3,4],preSumToIndex[4] = 1 + let preSumToIndex = new Map(); + let maxLen = 0; + // 前缀和数组(在这道题中可以优化为一个变量) + let preSum = 0; + // base case,这样索引相减的时候可以算出正确的子数组长度 + preSumToIndex.set(0, -1); + for (let i = 0; i < n; i++) { + // 计算前缀和,维护 preSum = sum(nums[0..i]) + preSum += nums[i]; + // 确保 preSumToIndex 中记录的索引是第一次出现的位置 + if (!preSumToIndex.has(preSum)) { + preSumToIndex.set(preSum, i); + } + let need = preSum - k; + if (preSumToIndex.has(need)) { + let j = preSumToIndex.get(need); + // nums[j..i] 是和为 k 的子数组 + maxLen = Math.max(maxLen, i - j); + } + } + return maxLen; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxSubArrayLen(self, nums: List[int], k: int) -> int: + n = len(nums) + # preSum 中的值 -> 对应的最小索引 + # 比如 preSum = [2,4,1,3,4],preSumToIndex[4] = 1 + preSumToIndex = {} + maxLen = 0 + # 前缀和数组(在这道题中可以优化为一个变量) + preSum = 0 + # base case,这样索引相减的时候可以算出正确的子数组长度 + preSumToIndex[0] = -1 + for i in range(n): + # 计算前缀和,维护 preSum = sum(nums[0..i]) + preSum += nums[i] + # 确保 preSumToIndex 中记录的索引是第一次出现的位置 + preSumToIndex.setdefault(preSum, i) + need = preSum - k + if need in preSumToIndex: + j = preSumToIndex[need] + # nums[j..i] 是和为 k 的子数组 + maxLen = max(maxLen, i - j) + return maxLen +``` + +https://leetcode.cn/problems/maximum-size-subarray-sum-equals-k 的多语言解法👆 + +https://leetcode.cn/problems/maximum-subarray 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxSubArray(vector& nums) { + int n = nums.size(); + if (n == 0) return 0; + vector dp(n); + // base case + // 第一个元素前面没有子数组 + dp[0] = nums[0]; + // 状态转移方程 + for (int i = 1; i < n; i++) { + dp[i] = max(nums[i], nums[i] + dp[i - 1]); + } + // 得到 nums 的最大子数组 + int res = INT_MIN; + for (int i = 0; i < n; i++) { + res = max(res, dp[i]); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func maxSubArray(nums []int) int { + n := len(nums) + if n == 0 { + return 0 + } + dp := make([]int, n) + // base case + // 第一个元素前面没有子数组 + dp[0] = nums[0] + // 状态转移方程 + for i := 1; i < n; i++ { + dp[i] = max(nums[i], nums[i] + dp[i - 1]) + } + // 得到 nums 的最大子数组 + res := math.MinInt32 + for i := 0; i < n; i++ { + res = max(res, dp[i]) + } + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxSubArray(int[] nums) { + int n = nums.length; + if (n == 0) return 0; + int[] dp = new int[n]; + // base case + // 第一个元素前面没有子数组 + dp[0] = nums[0]; + // 状态转移方程 + for (int i = 1; i < n; i++) { + dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]); + } + // 得到 nums 的最大子数组 + int res = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + res = Math.max(res, dp[i]); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number} + */ +var maxSubArray = function(nums) { + const n = nums.length; + if (n === 0) return 0; + const dp = new Array(n); + // base case + // 第一个元素前面没有子数组 + dp[0] = nums[0]; + // 状态转移方程 + for (let i = 1; i < n; i++) { + dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]); + } + // 得到 nums 的最大子数组 + let res = -Infinity; + for (let i = 0; i < n; i++) { + res = Math.max(res, dp[i]); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + n = len(nums) + if n == 0: + return 0 + dp = [0] * n + # base case + # 第一个元素前面没有子数组 + dp[0] = nums[0] + # 状态转移方程 + for i in range(1, n): + dp[i] = max(nums[i], nums[i] + dp[i - 1]) + # 得到 nums 的最大子数组 + res = float('-inf') + for i in range(n): + res = max(res, dp[i]) + return res +``` + +https://leetcode.cn/problems/maximum-subarray 的多语言解法👆 + +https://leetcode.cn/problems/maximum-sum-bst-in-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + int maxSum = 0; +public: + /* 主函数 */ + int maxSumBST(TreeNode* root) { + traverse(root); + return maxSum; + } + + vector findMaxMinSum(TreeNode* root) { + // base case + if (root == nullptr) { + return { + 1, INT_MAX, INT_MIN, 0 + }; + } + + // 递归计算左右子树 + vector left = findMaxMinSum(root->left); + vector right = findMaxMinSum(root->right); + + /*******后序遍历位置*******/ + vector res(4); + // 这个 if 在判断以 root 为根的二叉树是不是 BST + if (left[0] == 1 && right[0] == 1 && + root->val > left[2] && root->val < right[1]) { + // 以 root 为根的二叉树是 BST + res[0] = 1; + // 计算以 root 为根的这棵 BST 的最小值 + res[1] = min(left[1], root->val); + // 计算以 root 为根的这棵 BST 的最大值 + res[2] = max(right[2], root->val); + // 计算以 root 为根的这棵 BST 所有节点之和 + res[3] = left[3] + right[3] + root->val; + // 更新全局变量 + maxSum = max(maxSum, res[3]); + } else { + // 以 root 为根的二叉树不是 BST + res[0] = 0; + // 其他的值都没必要计算了,因为用不到 + } + /**************************/ + + return res; + } + + void traverse(TreeNode* root) { + if(root == nullptr) { + return; + } + vector res = findMaxMinSum(root); + traverse(root->left); + traverse(root->right); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +/* 主函数 */ +func maxSumBST(root *TreeNode) int { + // 全局变量,记录 BST 最大节点之和 + maxSum := 0 + traverse(root, &maxSum) + return maxSum +} + +func findMaxMinSum(root *TreeNode, maxSum *int) []int { + // base case + if root == nil { + return []int{1, math.MaxInt64, math.MinInt64, 0} + } + + // 递归计算左右子树 + left := findMaxMinSum(root.Left, maxSum) + right := findMaxMinSum(root.Right, maxSum) + + /*******后序遍历位置*******/ + res := make([]int, 4) + // 这个 if 在判断以 root 为根的二叉树是不是 BST + if left[0] == 1 && right[0] == 1 && + root.Val > left[2] && root.Val < right[1] { + // 以 root 为根的二叉树是 BST + res[0] = 1 + // 计算以 root 为根的这棵 BST 的最小值 + res[1] = int(math.Min(float64(left[1]), float64(root.Val))) + // 计算以 root 为根的这棵 BST 的最大值 + res[2] = int(math.Max(float64(right[2]), float64(root.Val))) + // 计算以 root 为根的这棵 BST 所有节点之和 + res[3] = left[3] + right[3] + root.Val + // 更新全局变量 + *maxSum = int(math.Max(float64(*maxSum), float64(res[3]))) + } else { + // 以 root 为根的二叉树不是 BST + res[0] = 0 + // 其他的值都没必要计算了,因为用不到 + } + /**************************/ + + return res +} + +func traverse(root *TreeNode, maxSum *int) { + if root == nil { + return + } + + findMaxMinSum(root, maxSum) + traverse(root.Left, maxSum) + traverse(root.Right, maxSum) +} +``` + +```java +// by labuladong (java) +class Solution { + // 全局变量,记录 BST 最大节点之和 + int maxSum = 0; + + /* 主函数 */ + public int maxSumBST(TreeNode root) { + traverse(root); + return maxSum; + } + + int[] findMaxMinSum(TreeNode root) { + // base case + if (root == null) { + return new int[] { + 1, Integer.MAX_VALUE, Integer.MIN_VALUE, 0 + }; + } + + // 递归计算左右子树 + int[] left = findMaxMinSum(root.left); + int[] right = findMaxMinSum(root.right); + + /*******后序遍历位置*******/ + int[] res = new int[4]; + // 这个 if 在判断以 root 为根的二叉树是不是 BST + if (left[0] == 1 && right[0] == 1 && + root.val > left[2] && root.val < right[1]) { + // 以 root 为根的二叉树是 BST + res[0] = 1; + // 计算以 root 为根的这棵 BST 的最小值 + res[1] = Math.min(left[1], root.val); + // 计算以 root 为根的这棵 BST 的最大值 + res[2] = Math.max(right[2], root.val); + // 计算以 root 为根的这棵 BST 所有节点之和 + res[3] = left[3] + right[3] + root.val; + // 更新全局变量 + maxSum = Math.max(maxSum, res[3]); + } else { + // 以 root 为根的二叉树不是 BST + res[0] = 0; + // 其他的值都没必要计算了,因为用不到 + } + /**************************/ + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxSumBST = function(root) { + + let maxSum = 0; + + const findMaxMinSum = node => { + if (node === null) { + return [1, Infinity, -Infinity, 0]; + } + + let left = findMaxMinSum(node.left); + let right = findMaxMinSum(node.right); + + /*******后序遍历位置*******/ + let res = new Array(4); + if (left[0] === 1 && right[0] === 1 && node.val > left[2] && node.val < right[1]) { + res[0] = 1; + res[1] = Math.min(left[1], node.val); + res[2] = Math.max(right[2], node.val); + res[3] = left[3] + right[3] + node.val; + maxSum = Math.max(maxSum, res[3]); + } else { + res[0] = 0; + } + /**************************/ + return res; + } + findMaxMinSum(root); + + return maxSum; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + # 全局变量,记录 BST 最大节点之和 + self.maxSum = 0 + + def maxSumBST(self, root: TreeNode) -> int: + self.traverse(root) + return self.maxSum + + def findMaxMinSum(self, root: TreeNode) -> List[int]: + # base case + if not root: + return [1, float('inf'), float('-inf'), 0] + + # 递归计算左右子树 + left = self.findMaxMinSum(root.left) + right = self.findMaxMinSum(root.right) + + /*******后序遍历位置*******/ + res = [0] * 4 + # 这个 if 在判断以 root 为根的二叉树是不是 BST + if left[0] == 1 and right[0] == 1 and root.val > left[2] and root.val < right[1]: + # 以 root 为根的二叉树是 BST + res[0] = 1 + # 计算以 root 为根的这棵 BST 的最小值 + res[1] = min(left[1], root.val) + # 计算以 root 为根的这棵 BST 的最大值 + res[2] = max(right[2], root.val) + # 计算以 root 为根的这棵 BST 所有节点之和 + res[3] = left[3] + right[3] + root.val + # 更新全局变量 + self.maxSum = max(self.maxSum, res[3]) + else: + # 以 root 为根的二叉树不是 BST + res[0] = 0 + # 其他的值都没必要计算了,因为用不到 + /**************************/ + return res +``` + +https://leetcode.cn/problems/maximum-sum-bst-in-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/merge-intervals 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> merge(vector>& intervals) { + vector> res; + // 按区间的 start 升序排列 + sort(intervals.begin(), intervals.end(), [](auto& a, auto& b){ + return a[0] < b[0]; + }); + + res.push_back(intervals[0]); + for (int i = 1; i < intervals.size(); i++) { + auto& curr = intervals[i]; + // res 中最后一个元素的引用 + auto& last = res.back(); + if (curr[0] <= last[1]) { + last[1] = max(last[1], curr[1]); + } else { + // 处理下一个待合并区间 + res.push_back(curr); + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func merge(intervals [][]int) [][]int { + res := [][]int{} + // 按区间的 start 升序排列 + sort.Slice(intervals, func(i, j int) bool { + return intervals[i][0] < intervals[j][0] + }) + + res = append(res, intervals[0]) + for i := 1; i < len(intervals); i++ { + curr := intervals[i] + // res 中最后一个元素的引用 + last := res[len(res) - 1] + if curr[0] <= last[1] { + last[1] = max(last[1], curr[1]) + } else { + // 处理下一个待合并区间 + res = append(res, curr) + } + } + return res +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] merge(int[][] intervals) { + LinkedList res = new LinkedList<>(); + // 按区间的 start 升序排列 + Arrays.sort(intervals, (a, b) -> { + return a[0] - b[0]; + }); + + res.add(intervals[0]); + for (int i = 1; i < intervals.length; i++) { + int[] curr = intervals[i]; + // res 中最后一个元素的引用 + int[] last = res.getLast(); + if (curr[0] <= last[1]) { + last[1] = Math.max(last[1], curr[1]); + } else { + // 处理下一个待合并区间 + res.add(curr); + } + } + return res.toArray(new int[0][0]); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} intervals + * @return {number[][]} + */ +var merge = function(intervals) { + const res = []; + // 按区间的 start 升序排列 + intervals.sort((a, b) => { + return a[0] - b[0]; + }); + + res.push(intervals[0]); + for (let i = 1; i < intervals.length; i++) { + const curr = intervals[i]; + // res 中最后一个元素的引用 + const last = res[res.length - 1]; + if (curr[0] <= last[1]) { + last[1] = Math.max(last[1], curr[1]); + } else { + // 处理下一个待合并区间 + res.push(curr); + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def merge(self, intervals: List[List[int]]) -> List[List[int]]: + res = [] + # 按区间的 start 升序排列 + intervals.sort(key=lambda a: a[0]) + + res.append(intervals[0]) + for curr in intervals[1:]: + # res 中最后一个元素的引用 + last = res[-1] + if curr[0] <= last[1]: + last[1] = max(last[1], curr[1]) + else: + # 处理下一个待合并区间 + res.append(curr) + return res +``` + +https://leetcode.cn/problems/merge-intervals 的多语言解法👆 + +https://leetcode.cn/problems/merge-k-sorted-lists 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* mergeKLists(vector& lists) { + int size = lists.size(); + // 虚拟头结点 + ListNode* dummy = new ListNode(-1); + ListNode* p = dummy; + // 优先级队列,最小堆 + priority_queue, compare > pq; + // 将 k 个链表的头结点加入最小堆 + for (auto head : lists) { + if (head) pq.push(head); + } + + while (!pq.empty()) { + // 获取最小节点,接到结果链表中 + auto node = pq.top(); + pq.pop(); + p->next = node; + if (node->next) { + pq.push(node->next); + } + // p 指针不断前进 + p = p->next; + } + return dummy->next; + } +private: + // 自定义比较函数 + struct compare { + bool operator() (ListNode* a, ListNode* b) { + return a->val > b->val; + } + }; +}; +``` + +```go +// by chatGPT (go) +//Definition for singly-linked list. + +func mergeKLists(lists []*ListNode) *ListNode { + if len(lists) == 0 { + return nil + } + // 虚拟头节点 + dummy := &ListNode{Val: -1} + p := dummy + // 优先队列,最小堆, 用golang的heap + pq := make(Queue, len(lists)) + for i, head := range lists { + if head != nil { + pq[i] = head + } + } + heap.Init(&pq) + + for pq.Len() != 0 { + // 获取最小节点,接到结果链表中 + node := heap.Pop(&pq).(*ListNode) + p.Next = node + if node.Next != nil { + heap.Push(&pq, node.Next) + } + // p 指针不断前进 + p = p.Next + } + return dummy.Next +} + +// golang的堆排序Queue +type Queue []*ListNode + +func (q Queue) Len() int { return len(q) } + +func (q Queue) Less(i, j int) bool { + return q[i].Val < q[j].Val +} + +func (q Queue) Swap(i, j int) { + q[i], q[j] = q[j], q[i] +} + +func (q *Queue) Push(x interface{}) { + *q = append(*q, x.(*ListNode)) +} + +func (q *Queue) Pop() interface{} { + old := *q + n := len(old) + x := old[n-1] + *q = old[:n-1] + return x +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode mergeKLists(ListNode[] lists) { + if (lists.length == 0) return null; + // 虚拟头结点 + ListNode dummy = new ListNode(-1); + ListNode p = dummy; + // 优先级队列,最小堆 + PriorityQueue pq = new PriorityQueue<>( + lists.length, (a, b)->(a.val - b.val)); + // 将 k 个链表的头结点加入最小堆 + for (ListNode head : lists) { + if (head != null) + pq.add(head); + } + + while (!pq.isEmpty()) { + // 获取最小节点,接到结果链表中 + ListNode node = pq.poll(); + p.next = node; + if (node.next != null) { + pq.add(node.next); + } + // p 指针不断前进 + p = p.next; + } + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** +* @param {ListNode[]} lists +* @return {ListNode} +*/ +var mergeKLists = function(lists) { + if (lists.length == 0) return null; + // 虚拟头结点 + let dummy = new ListNode(-1); + let p = dummy; + // 优先级队列,最小堆 + let pq = new PriorityQueue({ + compare: (a, b) => (a.val - b.val) + }); + // 将 k 个链表的头结点加入最小堆 + for (let head of lists) { + if (head != null) + pq.enqueue(head); + } + + while (!pq.isEmpty()) { + // 获取最小节点,接到结果链表中 + let node = pq.dequeue(); + p.next = node; + if (node.next != null) { + pq.enqueue(node.next); + } + // p 指针不断前进 + p = p.next; + } + return dummy.next; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def mergeKLists(self, lists: List[ListNode]) -> ListNode: + if not lists: + return None + # 虚拟头结点 + dummy = ListNode(-1) + p = dummy + # 优先级队列,最小堆 + pq = [] + for head in lists: + if head: + heapq.heappush(pq, (head.val, id(head), head)) + # 将 k 个链表的头结点加入最小堆 + while pq: + # 获取最小节点,接到结果链表中 + node = heapq.heappop(pq)[2] + p.next = node + if node.next: + heapq.heappush(pq, (node.next.val, id(node.next), node.next)) + # p 指针不断前进 + p = p.next + return dummy.next +``` + +https://leetcode.cn/problems/merge-k-sorted-lists 的多语言解法👆 + +https://leetcode.cn/problems/merge-sorted-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + void merge(vector& nums1, int m, vector& nums2, int n) { + // 两个指针分别初始化在两个数组的最后一个元素(类似拉链两端的锯齿) + int i = m - 1, j = n - 1; + // 生成排序的结果(类似拉链的拉锁) + int p = nums1.size() - 1; + // 从后向前生成结果数组,类似合并两个有序链表的逻辑 + while (i >= 0 && j >= 0) { + if (nums1[i] > nums2[j]) { + nums1[p] = nums1[i]; + i--; + } else { + nums1[p] = nums2[j]; + j--; + } + p--; + } + // 可能其中一个数组的指针走到尽头了,而另一个还没走完 + // 因为我们本身就是在往 nums1 中放元素,所以只需考虑 nums2 是否剩元素即可 + while (j >= 0) { + nums1[p] = nums2[j]; + j--; + p--; + } + } +}; +``` + +```go +// by chatGPT (go) +func merge(nums1 []int, m int, nums2 []int, n int) { + i, j, p := m-1, n-1, len(nums1)-1 // 初始化指针 + for i >= 0 && j >= 0 { // 两个数组都未遍历完时进行比较 + if nums1[i] > nums2[j] { // 挑选大的元素放入 nums1 的末位 + nums1[p] = nums1[i] + i-- + } else { + nums1[p] = nums2[j] + j-- + } + p-- // 从后往前生成结果 + } + for j >= 0 { // nums2 剩余元素放入 nums1 + nums1[p] = nums2[j] + j-- + p-- + } +} +``` + +```java +// by labuladong (java) +class Solution { + public void merge(int[] nums1, int m, int[] nums2, int n) { + // 两个指针分别初始化在两个数组的最后一个元素(类似拉链两端的锯齿) + int i = m - 1, j = n - 1; + // 生成排序的结果(类似拉链的拉锁) + int p = nums1.length - 1; + // 从后向前生成结果数组,类似合并两个有序链表的逻辑 + while (i >= 0 && j >= 0) { + if (nums1[i] > nums2[j]) { + nums1[p] = nums1[i]; + i--; + } else { + nums1[p] = nums2[j]; + j--; + } + p--; + } + // 可能其中一个数组的指针走到尽头了,而另一个还没走完 + // 因为我们本身就是在往 nums1 中放元素,所以只需考虑 nums2 是否剩元素即可 + while (j >= 0) { + nums1[p] = nums2[j]; + j--; + p--; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var merge = function(nums1, m, nums2, n) { + // 两个指针分别初始化在两个数组的最后一个元素(类似拉链两端的锯齿) + var i = m - 1, j = n - 1; + // 生成排序的结果(类似拉链的拉锁) + var p = nums1.length - 1; + // 从后向前生成结果数组,类似合并两个有序链表的逻辑 + while (i >= 0 && j >= 0) { + if (nums1[i] > nums2[j]) { + nums1[p] = nums1[i]; + i--; + } else { + nums1[p] = nums2[j]; + j--; + } + p--; + } + // 可能其中一个数组的指针走到尽头了,而另一个还没走完 + // 因为我们本身就是在往 nums1 中放元素,所以只需考虑 nums2 是否剩元素即可 + while (j >= 0) { + nums1[p] = nums2[j]; + j--; + p--; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: + """ + Do not return anything, modify nums1 in-place instead. + """ + # 两个指针分别初始化在两个数组的最后一个元素(类似拉链两端的锯齿) + i, j = m - 1, n - 1 + # 生成排序的结果(类似拉链的拉锁) + p = len(nums1) - 1 + # 从后向前生成结果数组,类似合并两个有序链表的逻辑 + while i >= 0 and j >= 0: + if nums1[i] > nums2[j]: + nums1[p] = nums1[i] + i -= 1 + else: + nums1[p] = nums2[j] + j -= 1 + p -= 1 + # 可能其中一个数组的指针走到尽头了,而另一个还没走完 + # 因为我们本身就是在往 nums1 中放元素,所以只需考虑 nums2 是否剩元素即可 + while j >= 0: + nums1[p] = nums2[j] + j -= 1 + p -= 1 +``` + +https://leetcode.cn/problems/merge-sorted-array 的多语言解法👆 + +https://leetcode.cn/problems/merge-two-sorted-lists 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { + // 虚拟头结点 + ListNode* dummy = new ListNode(-1), *p = dummy; + ListNode* p1 = l1, *p2 = l2; + + while (p1 != nullptr && p2 != nullptr) { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if (p1->val > p2->val) { + p->next = p2; + p2 = p2->next; + } else { + p->next = p1; + p1 = p1->next; + } + // p 指针不断前进 + p = p->next; + } + + if (p1 != nullptr) { + p->next = p1; + } + + if (p2 != nullptr) { + p->next = p2; + } + + return dummy->next; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode { + // 虚拟头结点 + dummy := &ListNode{-1, nil} + p := dummy + p1 := l1 + p2 := l2 + + for p1 != nil && p2 != nil { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if p1.Val > p2.Val { + p.Next = p2 + p2 = p2.Next + } else { + p.Next = p1 + p1 = p1.Next + } + // p 指针不断前进 + p = p.Next + } + + if p1 != nil { + p.Next = p1 + } + + if p2 != nil { + p.Next = p2 + } + + return dummy.Next +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + // 虚拟头结点 + ListNode dummy = new ListNode(-1), p = dummy; + ListNode p1 = l1, p2 = l2; + + while (p1 != null && p2 != null) { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if (p1.val > p2.val) { + p.next = p2; + p2 = p2.next; + } else { + p.next = p1; + p1 = p1.next; + } + // p 指针不断前进 + p = p.next; + } + + if (p1 != null) { + p.next = p1; + } + + if (p2 != null) { + p.next = p2; + } + + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} l1 + * @param {ListNode} l2 + * @return {ListNode} + */ +var mergeTwoLists = function(l1, l2) { + // 虚拟头结点 + var dummy = new ListNode(-1), p = dummy; + var p1 = l1, p2 = l2; + + while (p1 !== null && p2 !== null) { + /** + ![](../pictures/链表技巧/1.gif) + */ + // 比较 p1 和 p2 两个指针 + // 将值较小的的节点接到 p 指针 + if (p1.val > p2.val) { + p.next = p2; + p2 = p2.next; + } else { + p.next = p1; + p1 = p1.next; + } + // p 指针不断前进 + p = p.next; + } + + if (p1 !== null) { + p.next = p1; + } + + if (p2 !== null) { + p.next = p2; + } + + return dummy.next; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: + # 虚拟头结点 + dummy = ListNode(-1) + p = dummy + p1 = l1 + p2 = l2 + + while p1 and p2: + """ + + ![](../pictures/链表技巧/1.gif) + """ + # 比较 p1 和 p2 两个指针 + # 将值较小的的节点接到 p 指针 + if p1.val > p2.val: + p.next = p2 + p2 = p2.next + else: + p.next = p1 + p1 = p1.next + # p 指针不断前进 + p = p.next + + if p1: + p.next = p1 + + if p2: + p.next = p2 + + return dummy.next +``` + +https://leetcode.cn/problems/merge-two-sorted-lists 的多语言解法👆 + +https://leetcode.cn/problems/middle-of-the-linked-list 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* middleNode(ListNode* head) { + // 快慢指针初始化指向 head + ListNode* slow = head; + ListNode* fast = head; + // 快指针走到末尾时停止 + while (fast != nullptr && fast->next != nullptr) { + // 慢指针走一步,快指针走两步 + slow = slow->next; + fast = fast->next->next; + } + // 慢指针指向中点 + return slow; + } +}; +``` + +```go +// by chatGPT (go) +func middleNode(head *ListNode) *ListNode { + // 快慢指针初始化指向 head + slow, fast := head, head + // 快指针走到末尾时停止 + for fast != nil && fast.Next != nil { + // 慢指针走一步,快指针走两步 + slow = slow.Next + fast = fast.Next.Next + } + // 慢指针指向中点 + return slow +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode middleNode(ListNode head) { + // 快慢指针初始化指向 head + ListNode slow = head, fast = head; + // 快指针走到末尾时停止 + while (fast != null && fast.next != null) { + // 慢指针走一步,快指针走两步 + slow = slow.next; + fast = fast.next.next; + } + // 慢指针指向中点 + return slow; + } +} +``` + +```javascript +// by chatGPT (javascript) +var middleNode = function(head) { + // 快慢指针初始化指向 head + let slow = head, fast = head; + // 快指针走到末尾时停止 + while (fast != null && fast.next != null) { + // 慢指针走一步,快指针走两步 + slow = slow.next; + fast = fast.next.next; + } + // 慢指针指向中点 + return slow; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def middleNode(self, head: ListNode) -> ListNode: + # 快慢指针初始化指向 head + slow = fast = head + # 快指针走到末尾时停止 + while fast and fast.next: + # 慢指针走一步,快指针走两步 + slow = slow.next + fast = fast.next.next + # 慢指针指向中点 + return slow +``` + +https://leetcode.cn/problems/middle-of-the-linked-list 的多语言解法👆 + +https://leetcode.cn/problems/min-cost-to-connect-all-points 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class UF { +public: + // 连通分量个数 + int count; + // 存储一棵树 + vector parent; + // 记录树的「重量」 + vector size; + + // n 为图中节点的个数 + UF(int n) { + this->count = n; + parent.resize(n); + size.resize(n); + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + // 将节点 p 和节点 q 连通 + void unionn(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + bool connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + // 返回节点 x 的连通分量根节点 + int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + int getCount() { + return this->count; + } +}; + +class Solution { +public: + int minCostConnectPoints(vector>& points) { + int n = points.size(); + // 生成所有边及权重 + vector> edges; + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + int xi = points[i][0], yi = points[i][1]; + int xj = points[j][0], yj = points[j][1]; + // 用坐标点在 points 中的索引表示坐标点 + edges.push_back({i, j, abs(xi - xj) + abs(yi - yj)}); + } + } + // 将边按照权重从小到大排序 + sort(edges.begin(), edges.end(), [](auto& a, auto& b){ + return a[2] < b[2]; + }); + // 执行 Kruskal 算法 + int mst = 0; + UF uf(n); + for (auto& edge : edges) { + int u = edge[0]; + int v = edge[1]; + int weight = edge[2]; + // 若这条边会产生环,则不能加入 mst + if (uf.connected(u, v)) { + continue; + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight; + uf.unionn(u, v); + } + return mst; + } +}; +``` + +```go +// by chatGPT (go) +func minCostConnectPoints(points [][]int) int { + n := len(points) + // 生成所有边及权重 + edges := make([][]int, 0) + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + xi, yi := points[i][0], points[i][1] + xj, yj := points[j][0], points[j][1] + // 用坐标点在 points 中的索引表示坐标点 + edges = append(edges, []int{i, j, abs(xi - xj) + abs(yi - yj)}) + } + } + // 将边按照权重从小到大排序 + sort.Slice(edges, func(i, j int) bool { + return edges[i][2] < edges[j][2] + }) + // 执行 Kruskal 算法 + mst := 0 + uf := NewUF(n) + for _, edge := range edges { + u, v, weight := edge[0], edge[1], edge[2] + // 若这条边会产生环,则不能加入 mst + if uf.connected(u, v) { + continue + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight + uf.union(u, v) + } + return mst +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +type UF struct { + // 连通分量个数 + count int + // 存储一棵树 + parent []int + // 记录树的「重量」 + size []int +} + +// NewUF returns a new UF with n nodes +func NewUF(n int) *UF { + uf := &UF{} + uf.count = n + uf.parent = make([]int, n) + uf.size = make([]int, n) + for i := 0; i < n; i++ { + uf.parent[i] = i + uf.size[i] = 1 + } + return uf +} + +func (uf *UF) union(p, q int) { + rootP := uf.find(p) + rootQ := uf.find(q) + if rootP == rootQ { + return + } + // 小树接到大树下面,较平衡 + if uf.size[rootP] > uf.size[rootQ] { + uf.parent[rootQ] = rootP + uf.size[rootP] += uf.size[rootQ] + } else { + uf.parent[rootP] = rootQ + uf.size[rootQ] += uf.size[rootP] + } + // 两个连通分量合并成一个连通分量 + uf.count-- +} + +func (uf *UF) connected(p, q int) bool { + rootP := uf.find(p) + rootQ := uf.find(q) + return rootP == rootQ +} + +func (uf *UF) find(x int) int { + for uf.parent[x] != x { + // 进行路径压缩 + uf.parent[x], x = uf.parent[uf.parent[x]], uf.parent[uf.parent[x]] + } + return x +} + +func (uf *UF) Count() int { + return uf.count +} +``` + +```java +// by labuladong (java) +class Solution { + public int minCostConnectPoints(int[][] points) { + int n = points.length; + // 生成所有边及权重 + List edges = new ArrayList<>(); + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + int xi = points[i][0], yi = points[i][1]; + int xj = points[j][0], yj = points[j][1]; + // 用坐标点在 points 中的索引表示坐标点 + edges.add(new int[]{ + i, j, Math.abs(xi - xj) + Math.abs(yi - yj) + }); + } + } + // 将边按照权重从小到大排序 + Collections.sort(edges, (a, b) -> { + return a[2] - b[2]; + }); + // 执行 Kruskal 算法 + int mst = 0; + UF uf = new UF(n); + for (int[] edge : edges) { + int u = edge[0]; + int v = edge[1]; + int weight = edge[2]; + // 若这条边会产生环,则不能加入 mst + if (uf.connected(u, v)) { + continue; + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight; + uf.union(u, v); + } + return mst; + } + + class UF { + // 连通分量个数 + private int count; + // 存储一棵树 + private int[] parent; + // 记录树的「重量」 + private int[] size; + + // n 为图中节点的个数 + public UF(int n) { + this.count = n; + parent = new int[n]; + size = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + // 将节点 p 和节点 q 连通 + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + // 返回节点 x 的连通分量根节点 + private int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + public int count() { + return count; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var minCostConnectPoints = function(points) { + const n = points.length; + // 生成所有边及权重 + let edges = []; + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + const xi = points[i][0], yi = points[i][1]; + const xj = points[j][0], yj = points[j][1]; + // 用坐标点在 points 中的索引表示坐标点 + edges.push([i, j, Math.abs(xi - xj) + Math.abs(yi - yj)]); + } + } + // 将边按照权重从小到大排序 + edges.sort((a, b) => { + return a[2] - b[2]; + }); + // 执行 Kruskal 算法 + let mst = 0; + const uf = new UF(n); + for (let i = 0; i < edges.length; i++) { + const u = edges[i][0]; + const v = edges[i][1]; + const weight = edges[i][2]; + // 若这条边会产生环,则不能加入 mst + if (uf.connected(u, v)) { + continue; + } + // 若这条边不会产生环,则属于最小生成树 + mst += weight; + uf.union(u, v); + } + return mst; +}; + +class UF { + // 连通分量个数 + count = 0; + // 存储一棵树 + parent = []; + // 记录树的「重量」 + size = []; + + // n 为图中节点的个数 + constructor(n) { + this.count = n; + this.parent = new Array(n).fill(0).map((_, index) => index); + this.size = new Array(n).fill(1); + } + + // 将节点 p 和节点 q 连通 + union(p, q) { + let rootP = this.find(p); + let rootQ = this.find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (this.size[rootP] > this.size[rootQ]) { + this.parent[rootQ] = rootP; + this.size[rootP] += this.size[rootQ]; + } else { + this.parent[rootP] = rootQ; + this.size[rootQ] += this.size[rootP]; + } + // 两个连通分量合并成一个连通分量 + this.count--; + } + + // 判断节点 p 和节点 q 是否连通 + connected(p, q) { + let rootP = this.find(p); + let rootQ = this.find(q); + return rootP == rootQ; + } + + // 返回节点 x 的连通分量根节点 + find(x) { + while (this.parent[x] != x) { + // 进行路径压缩 + this.parent[x] = this.parent[this.parent[x]]; + x = this.parent[x]; + } + return x; + } + + // 返回图中的连通分量个数 + count() { + return this.count; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def minCostConnectPoints(self, points: List[List[int]]) -> int: + n = len(points) + # 生成所有边及权重 + edges = [] + for i in range(n): + for j in range(i + 1, n): + xi, yi = points[i][0], points[i][1] + xj, yj = points[j][0], points[j][1] + # 用坐标点在 points 中的索引表示坐标点 + edges.append([i, j, abs(xi - xj) + abs(yi - yj)]) + # 将边按照权重从小到大排序 + edges.sort(key=lambda x: x[2]) + # 执行 Kruskal 算法 + mst = 0 + uf = UF(n) + for edge in edges: + u = edge[0] + v = edge[1] + weight = edge[2] + # 若这条边会产生环,则不能加入 mst + if uf.connected(u, v): + continue + # 若这条边不会产生环,则属于最小生成树 + mst += weight + uf.union(u, v) + return mst + +class UF: + # 连通分量个数 + count = 0 + # 存储一棵树 + parent = [] + # 记录树的「重量」 + size = [] + + def __init__(self, n: int): + # n 为图中节点的个数 + self.count = n + self.parent = [i for i in range(n)] + self.size = [1] * n + + # 将节点 p 和节点 q 连通 + def union(self, p: int, q: int) -> None: + root_p = self.find(p) + root_q = self.find(q) + if root_p == root_q: + return + + # 小树接到大树下面,较平衡 + if self.size[root_p] > self.size[root_q]: + self.parent[root_q] = root_p + self.size[root_p] += self.size[root_q] + else: + self.parent[root_p] = root_q + self.size[root_q] += self.size[root_p] + + # 两个连通分量合并成一个连通分量 + self.count -= 1 + + # 判断节点 p 和节点 q 是否连通 + def connected(self, p: int, q: int) -> bool: + root_p = self.find(p) + root_q = self.find(q) + return root_p == root_q + + # 返回节点 x 的连通分量根节点 + def find(self, x: int) -> int: + while self.parent[x] != x: + # 进行路径压缩 + self.parent[x] = self.parent[self.parent[x]] + x = self.parent[x] + return x + + # 返回图中的连通分量个数 + def count(self) -> int: + return self.count +``` + +https://leetcode.cn/problems/min-cost-to-connect-all-points 的多语言解法👆 + +https://leetcode.cn/problems/min-stack 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class MinStack { + // 记录栈中的所有元素 + stack stk; + // 阶段性记录栈中的最小元素 + stack minStk; + +public: + void push(int val) { + stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (minStk.empty() || val <= minStk.top()) { + // 新插入的这个元素就是全栈最小的 + minStk.push(val); + } + } + + void pop() { + // 弹出的元素是全栈最小的 + if (stk.top() == minStk.top()) { + minStk.pop(); + } + stk.pop(); + } + + int top() { + return stk.top(); + } + + int getMin() { + // minStk 栈顶为全栈最小元素 + return minStk.top(); + } +}; +``` + +```go +// by chatGPT (go) +// 原始思路 +type MinStack1 struct { + // 记录栈中的所有元素 + stk []int + // 阶段性记录栈中的最小元素 + minStk []int +} + +/** initialize your data structure here. */ +func Constructor1() MinStack1 { + return MinStack1{} +} + +func (this *MinStack1) Push(val int) { + this.stk = append(this.stk, val) + // 维护 minStk 栈顶为全栈最小元素 + if len(this.minStk) == 0 || val <= this.minStk[len(this.minStk)-1] { + // 新插入的这个元素就是全栈最小的 + this.minStk = append(this.minStk, val) + } else { + // 插入的这个元素比较大 + this.minStk = append(this.minStk, this.minStk[len(this.minStk)-1]) + } +} + +func (this *MinStack1) Pop() { + this.stk = this.stk[:len(this.stk)-1] + this.minStk = this.minStk[:len(this.minStk)-1] +} + +func (this *MinStack1) Top() int { + return this.stk[len(this.stk)-1] +} + +func (this *MinStack1) GetMin() int { + // minStk 栈顶为全栈最小元素 + return this.minStk[len(this.minStk)-1] +} + +// 优化版 +type MinStack struct { + // 记录栈中的所有元素 + stk []int + // 阶段性记录栈中的最小元素 + minStk []int +} + +/** initialize your data structure here. */ +func Constructor() MinStack { + return MinStack{} +} + +func (this *MinStack) Push(val int) { + this.stk = append(this.stk, val) + // 维护 minStk 栈顶为全栈最小元素 + if len(this.minStk) == 0 || val <= this.minStk[len(this.minStk)-1] { + // 新插入的这个元素就是全栈最小的 + this.minStk = append(this.minStk, val) + } +} + +func (this *MinStack) Pop() { + // 注意 Go 语言的语言特性,比较 int 相等直接用 == + if this.stk[len(this.stk)-1] == this.minStk[len(this.minStk)-1] { + // 弹出的元素是全栈最小的 + this.minStk = this.minStk[:len(this.minStk)-1] + } + this.stk = this.stk[:len(this.stk)-1] +} + +func (this *MinStack) Top() int { + return this.stk[len(this.stk)-1] +} + +func (this *MinStack) GetMin() int { + // minStk 栈顶为全栈最小元素 + return this.minStk[len(this.minStk)-1] +} +``` + +```java +// by labuladong (java) +// 原始思路 +class MinStack1 { + // 记录栈中的所有元素 + Stack stk = new Stack<>(); + // 阶段性记录栈中的最小元素 + Stack minStk = new Stack<>(); + + public void push(int val) { + stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (minStk.isEmpty() || val <= minStk.peek()) { + // 新插入的这个元素就是全栈最小的 + minStk.push(val); + } else { + // 插入的这个元素比较大 + minStk.push(minStk.peek()); + } + } + + public void pop() { + stk.pop(); + minStk.pop(); + } + + public int top() { + return stk.peek(); + } + + public int getMin() { + // minStk 栈顶为全栈最小元素 + return minStk.peek(); + } +} +// 优化版 +class MinStack { + // 记录栈中的所有元素 + Stack stk = new Stack<>(); + // 阶段性记录栈中的最小元素 + Stack minStk = new Stack<>(); + + public void push(int val) { + stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (minStk.isEmpty() || val <= minStk.peek()) { + // 新插入的这个元素就是全栈最小的 + minStk.push(val); + } + } + + public void pop() { + // 注意 Java 的语言特性,比较 Integer 相等要用 equals 方法 + if (stk.peek().equals(minStk.peek())) { + // 弹出的元素是全栈最小的 + minStk.pop(); + } + stk.pop(); + } + + public int top() { + return stk.peek(); + } + + public int getMin() { + // minStk 栈顶为全栈最小元素 + return minStk.peek(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * initialize your data structure here. + */ +var MinStack = function() { + // 记录栈中的所有元素 + this.stk = []; + // 阶段性记录栈中的最小元素 + this.minStk = []; +}; + +/** + * @param {number} val + * @return {void} + */ +MinStack.prototype.push = function(val) { + this.stk.push(val); + // 维护 minStk 栈顶为全栈最小元素 + if (this.minStk.length == 0 || val <= this.minStk[this.minStk.length - 1]) { + // 新插入的这个元素就是全栈最小的 + this.minStk.push(val); + } +}; + +/** + * @return {void} + */ +MinStack.prototype.pop = function() { + // 注意 JavaScript 的语言特性,比较 Number 相等不需要用 equals 方法 + if (this.stk[this.stk.length - 1] == this.minStk[this.minStk.length - 1]) { + // 弹出的元素是全栈最小的 + this.minStk.pop(); + } + this.stk.pop(); +}; + +/** + * @return {number} + */ +MinStack.prototype.top = function() { + return this.stk[this.stk.length - 1]; +}; + +/** + * @return {number} + */ +MinStack.prototype.getMin = function() { + // minStk 栈顶为全栈最小元素 + return this.minStk[this.minStk.length - 1]; +}; +``` + +```python +# by chatGPT (python) +class MinStack1: + # 记录栈中的所有元素 + def __init__(self): + self.stk = [] + # 阶段性记录栈中的最小元素 + self.minStk = [] + + def push(self, val): + self.stk.append(val) + # 维护 minStk 栈顶为全栈最小元素 + if not self.minStk or val <= self.minStk[-1]: + # 新插入的这个元素就是全栈最小的 + self.minStk.append(val) + else: + # 插入的这个元素比较大 + self.minStk.append(self.minStk[-1]) + + def pop(self): + self.stk.pop() + self.minStk.pop() + + def top(self): + return self.stk[-1] + + def getMin(self): + # minStk 栈顶为全栈最小元素 + return self.minStk[-1] + + +# 优化版 +class MinStack: + # 记录栈中的所有元素 + def __init__(self): + self.stk = [] + # 阶段性记录栈中的最小元素 + self.minStk = [] + + def push(self, val): + self.stk.append(val) + # 维护 minStk 栈顶为全栈最小元素 + if not self.minStk or val <= self.minStk[-1]: + # 新插入的这个元素就是全栈最小的 + self.minStk.append(val) + + def pop(self): + # 注意 Python 语言相等比较可以使用 "==" 操作符 + if self.stk[-1] == self.minStk[-1]: + # 弹出的元素是全栈最小的 + self.minStk.pop() + self.stk.pop() + + def top(self): + return self.stk[-1] + + def getMin(self): + # minStk 栈顶为全栈最小元素 + return self.minStk[-1] +``` + +https://leetcode.cn/problems/min-stack 的多语言解法👆 + +https://leetcode.cn/problems/minimum-absolute-difference-in-bst 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int getMinimumDifference(TreeNode* root) { + traverse(root); + return res; + } + + TreeNode* prev = nullptr; + int res = INT_MAX; + + // 遍历函数 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + traverse(root->left); + // 中序遍历位置 + if (prev != nullptr) { + res = min(res, root->val - prev->val); + } + prev = root; + traverse(root->right); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +func getMinimumDifference(root *TreeNode) int { + prev := (*TreeNode)(nil) + res := math.MaxInt32 + + var traverse func(root *TreeNode) + traverse = func(root *TreeNode) { + if root == nil { + return + } + + traverse(root.Left) + // 中序遍历位置 + if prev != nil { + res = min(res, root.Val - prev.Val) + } + + prev = root + traverse(root.Right) + } + + traverse(root) + return res +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + +```java +// by labuladong (java) +class Solution { + public int getMinimumDifference(TreeNode root) { + traverse(root); + return res; + } + + TreeNode prev = null; + int res = Integer.MAX_VALUE; + + // 遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.left); + // 中序遍历位置 + if (prev != null) { + res = Math.min(res, root.val - prev.val); + } + prev = root; + traverse(root.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {number} + */ +var getMinimumDifference = function(root) { + let prev = null; + let res = Number.MAX_SAFE_INTEGER; + + // 遍历函数 + function traverse(root) { + if (root == null) { + return; + } + traverse(root.left); + // 中序遍历位置 + if (prev != null) { + res = Math.min(res, root.val - prev.val); + } + prev = root; + traverse(root.right); + } + + traverse(root); + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def getMinimumDifference(self, root: TreeNode) -> int: + self.prev = None + self.res = float('inf') + self.traverse(root) + return self.res + + # 遍历函数 + def traverse(self, root: TreeNode) -> None: + if not root: + return + + self.traverse(root.left) + # 中序遍历位置 + if self.prev: + self.res = min(self.res, root.val - self.prev.val) + self.prev = root + self.traverse(root.right) +``` + +https://leetcode.cn/problems/minimum-absolute-difference-in-bst 的多语言解法👆 + +https://leetcode.cn/problems/minimum-add-to-make-parentheses-valid 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minAddToMakeValid(string s) { + // res 记录插入次数 + int res = 0; + // need 变量记录右括号的需求量 + int need = 0; + + for (int i = 0; i < s.length(); i++) { + if (s[i] == '(') { + // 对右括号的需求 + 1 + need++; + } + + if (s[i] == ')') { + // 对右括号的需求 - 1 + need--; + + if (need == -1) { + need = 0; + // 需插入一个左括号 + res++; + } + } + } + + return res + need; + } +}; +``` + +```go +// by chatGPT (go) +func minAddToMakeValid(s string) int { + // res 记录插入次数 + res := 0 + // need 变量记录右括号的需求量 + need := 0 + + for i := 0; i < len(s); i++ { + if s[i] == '(' { + // 对右括号的需求 + 1 + need++ + } + + if s[i] == ')' { + // 对右括号的需求 - 1 + need-- + + if need == -1 { + need = 0 + // 需插入一个左括号 + res++ + } + } + } + + return res + need +} +``` + +```java +// by labuladong (java) +class Solution { + public int minAddToMakeValid(String s) { + // res 记录插入次数 + int res = 0; + // need 变量记录右括号的需求量 + int need = 0; + + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + // 对右括号的需求 + 1 + need++; + } + + if (s.charAt(i) == ')') { + // 对右括号的需求 - 1 + need--; + + if (need == -1) { + need = 0; + // 需插入一个左括号 + res++; + } + } + } + + return res + need; + } +} +``` + +```javascript +// by chatGPT (javascript) +var minAddToMakeValid = function(s) { + // res 记录插入次数 + let res = 0; + // need 变量记录右括号的需求量 + let need = 0; + + for (let i = 0; i < s.length; i++) { + if (s.charAt(i) == '(') { + // 对右括号的需求 + 1 + need++; + } + + if (s.charAt(i) == ')') { + // 对右括号的需求 - 1 + need--; + + if (need == -1) { + need = 0; + // 需插入一个左括号 + res++; + } + } + } + + return res + need; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minAddToMakeValid(self, s: str) -> int: + # res 记录插入次数 + res = 0 + # need 变量记录右括号的需求量 + need = 0 + + for i in range(len(s)): + if s[i] == '(': + # 对右括号的需求 + 1 + need += 1 + + if s[i] == ')': + # 对右括号的需求 - 1 + need -= 1 + + if need == -1: + need = 0 + # 需插入一个左括号 + res += 1 + + return res + need +``` + +https://leetcode.cn/problems/minimum-add-to-make-parentheses-valid 的多语言解法👆 + +https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 备忘录 + int memo[1001][1001]; + + /* 主函数 */ + int minimumDeleteSum(string s1, string s2) { + int m = s1.size(), n = s2.size(); + // 备忘录值为 -1 代表未曾计算 + memset(memo, -1, sizeof(memo)); + return dp(s1, 0, s2, 0); + } + + // 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串, + // 最小的 ASCII 码之和为 dp(s1, i, s2, j)。 + int dp(string s1, int i, string s2, int j) { + int res = 0; + // base case + if (i == s1.size()) { + // 如果 s1 到头了,那么 s2 剩下的都得删除 + for (; j < s2.size(); j++) + res += s2[j]; + return res; + } + if (j == s2.size()) { + // 如果 s2 到头了,那么 s1 剩下的都得删除 + for (; i < s1.size(); i++) + res += s1[i]; + return res; + } + + if (memo[i][j] != -1) { + return memo[i][j]; + } + + if (s1[i] == s2[j]) { + // s1[i] 和 s2[j] 都是在 lcs 中的,不用删除 + memo[i][j] = dp(s1, i + 1, s2, j + 1); + } else { + // s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个 + memo[i][j] = min( + s1[i] + dp(s1, i + 1, s2, j), + s2[j] + dp(s1, i, s2, j + 1) + ); + } + return memo[i][j]; + } +}; +``` + +```go +// by chatGPT (go) +func minimumDeleteSum(s1 string, s2 string) int { + m, n := len(s1), len(s2) + // 备忘录值为 -1 代表未曾计算 + memo := make([][]int, m) + for i := range memo { + memo[i] = make([]int, n) + for j := range memo[i] { + memo[i][j] = -1 + } + } + + var dp func(s1 string, i int, s2 string, j int) int + // 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串, + // 最小的 ASCII 码之和为 dp(s1, i, s2, j)。 + dp = func(s1 string, i int, s2 string, j int) int { + res := 0 + // base case + if i == len(s1) { + // 如果 s1 到头了,那么 s2 剩下的都得删除 + for ; j < len(s2); j++ { + res += int(s2[j]) + } + return res + } + if j == len(s2) { + // 如果 s2 到头了,那么 s1 剩下的都得删除 + for ; i < len(s1); i++ { + res += int(s1[i]) + } + return res + } + + if memo[i][j] != -1 { + return memo[i][j] + } + + if s1[i] == s2[j] { + // s1[i] 和 s2[j] 都是在 lcs 中的,不用删除 + memo[i][j] = dp(s1, i + 1, s2, j + 1) + } else { + // s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个 + memo[i][j] = min( + int(s1[i]) + dp(s1, i + 1, s2, j), + int(s2[j]) + dp(s1, i, s2, j + 1), + ) + } + return memo[i][j] + } + + return dp(s1, 0, s2, 0) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + + // 备忘录 + int memo[][]; + + /* 主函数 */ + public int minimumDeleteSum(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 备忘录值为 -1 代表未曾计算 + memo = new int[m][n]; + for (int[] row : memo) + Arrays.fill(row, -1); + + return dp(s1, 0, s2, 0); + } + + // 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串, + // 最小的 ASCII 码之和为 dp(s1, i, s2, j)。 + int dp(String s1, int i, String s2, int j) { + int res = 0; + // base case + if (i == s1.length()) { + // 如果 s1 到头了,那么 s2 剩下的都得删除 + for (; j < s2.length(); j++) + res += s2.charAt(j); + return res; + } + if (j == s2.length()) { + // 如果 s2 到头了,那么 s1 剩下的都得删除 + for (; i < s1.length(); i++) + res += s1.charAt(i); + return res; + } + + if (memo[i][j] != -1) { + return memo[i][j]; + } + + if (s1.charAt(i) == s2.charAt(j)) { + // s1[i] 和 s2[j] 都是在 lcs 中的,不用删除 + memo[i][j] = dp(s1, i + 1, s2, j + 1); + } else { + // s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个 + memo[i][j] = Math.min( + s1.charAt(i) + dp(s1, i + 1, s2, j), + s2.charAt(j) + dp(s1, i, s2, j + 1) + ); + } + return memo[i][j]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var minimumDeleteSum = function(s1, s2) { + var m = s1.length, n = s2.length; + // memo[i][j] 存储将s1[i...]和s2[j...]删除成相同字符串的最小ASCII码之和 + var memo = new Array(m); + for(var i=0; i int: + m, n = len(s1), len(s2) + # 备忘录值为 -1 代表未曾计算 + memo = [[-1] * n for _ in range(m)] + + def dp(i: int, j: int) -> int: + res = 0 + # base case + if i == m: + # 如果 s1 到头了,那么 s2 剩下的都得删除 + for k in range(j, n): + res += ord(s2[k]) + return res + if j == n: + # 如果 s2 到头了,那么 s1 剩下的都得删除 + for k in range(i, m): + res += ord(s1[k]) + return res + + if memo[i][j] != -1: + return memo[i][j] + + if s1[i] == s2[j]: + # s1[i] 和 s2[j] 都是在 lcs 中的,不用删除 + memo[i][j] = dp(i + 1, j + 1) + else: + # s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个 + memo[i][j] = min( + ord(s1[i]) + dp(i + 1, j), + ord(s2[j]) + dp(i, j + 1) + ) + return memo[i][j] + + return dp(0, 0) +``` + +https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings 的多语言解法👆 + +https://leetcode.cn/problems/minimum-depth-of-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minDepth(TreeNode* root) { + if (root == NULL) return 0; + queue q; + q.push(root); + // root 本身就是一层,depth 初始化为 1 + int depth = 1; + + while (!q.empty()) { + /* 层数 step */ + int sz = q.size(); + /* 将当前队列中的所有节点向四周扩散 */ + for (int i = 0; i < sz; i++) { + TreeNode* cur = q.front(); + q.pop(); + /* 判断是否到达终点 */ + if (cur->left == NULL && cur->right == NULL) + return depth; + /* 将 cur 的相邻节点加入队列 */ + if (cur->left != NULL) + q.push(cur->left); + if (cur->right != NULL) + q.push(cur->right); + } + /* 这里增加步数 */ + depth++; + } + return depth; + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for TreeNode. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func minDepth(root *TreeNode) int { + if root == nil { + return 0 + } + q := []*TreeNode{root} + // root 本身就是一层,depth 初始化为 1 + depth := 1 + + for len(q) != 0 { + /** + ![](../pictures/dijkstra/1.jpeg) + */ + sz := len(q) + /* 遍历当前层的节点 */ + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + /* 判断是否到达叶子结点 */ + if cur.Left == nil && cur.Right == nil { + return depth + } + /* 将下一层节点加入队列 */ + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + /* 这里增加步数 */ + depth++ + } + return depth +} +``` + +```java +// by labuladong (java) +// 「迭代」的递归思路 +class Solution { + private int minDepth = Integer.MAX_VALUE; + private int currentDepth = 0; + + public int minDepth(TreeNode root) { + if (root == null) { + return 0; + } + traverse(root); + return minDepth; + } + + private void traverse(TreeNode root) { + if (root == null) { + return; + } + + // 做选择:在进入节点时增加当前深度 + currentDepth++; + + // 如果当前节点是叶子节点,更新最小深度 + if (root.left == null && root.right == null) { + minDepth = Math.min(minDepth, currentDepth); + } + + traverse(root.left); + traverse(root.right); + + // 撤销选择:在离开节点时减少当前深度 + currentDepth--; + } +} + +// 「分解问题」的递归思路 +class Solution2 { + public int minDepth(TreeNode root) { + // 基本情况:如果节点为空,返回深度为0 + if (root == null) { + return 0; + } + + // 递归计算左子树的最小深度 + int leftDepth = minDepth(root.left); + // 递归计算右子树的最小深度 + int rightDepth = minDepth(root.right); + + // 特殊情况处理:如果左子树为空,返回右子树的深度加1 + if (leftDepth == 0) { + return rightDepth + 1; + } + // 特殊情况处理:如果右子树为空,返回左子树的深度加1 + if (rightDepth == 0) { + return leftDepth + 1; + } + + // 计算并返回最小深度:左右子树深度的最小值加1 + return Math.min(leftDepth, rightDepth) + 1; + } +} + + +// BFS 的思路 +class Solution3 { + public int minDepth(TreeNode root) { + if (root == null) return 0; + Queue q = new LinkedList<>(); + q.offer(root); + // root 本身就是一层,depth 初始化为 1 + int depth = 1; + + while (!q.isEmpty()) { + /** + ![](../pictures/dijkstra/1.jpeg) + */ + int sz = q.size(); + /* 遍历当前层的节点 */ + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + /* 判断是否到达叶子结点 */ + if (cur.left == null && cur.right == null) + return depth; + /* 将下一层节点加入队列 */ + if (cur.left != null) + q.offer(cur.left); + if (cur.right != null) + q.offer(cur.right); + } + /* 这里增加步数 */ + depth++; + } + return depth; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {number} + */ +var minDepth = function(root) { + if (root === null) return 0; + var q = []; + q.push(root); + //root本身就是一层,depth初始化为 1 + var depth = 1; + + while (q.length !== 0) { + /** + ![](../pictures/dijkstra/1.jpeg) + */ + var sz = q.length; + /* 遍历当前层的节点 */ + for (var i = 0; i < sz; i++) { + + var cur = q.shift(); + /* 判断是否到达叶子结点 */ + if (!cur.left && !cur.right) { + return depth; + } + /* 将下一层节点加入队列 + */ + if (cur.left !== null) { + q.push(cur.left); + } + if (cur.right !== null) { + q.push(cur.right); + } + + } + /* 这里增加步数 */ + depth++; + } + return depth; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minDepth(self, root: TreeNode) -> int: + if not root: + return 0 + q = collections.deque([root]) + # root 本身就是一层,depth 初始化为 1 + depth = 1 + while q: + # extend down -200> + # ![](../pictures/dijkstra/1.jpeg) + sz = len(q) + ## 遍历当前层的节点 + for i in range(sz): + cur = q.popleft() + ## 判断是否到达叶子结点 + if not cur.left and not cur.right: + return depth + ## 将下一层节点加入队列 + if cur.left: + q.append(cur.left) + if cur.right: + q.append(cur.right) + ## 这里增加步数 + depth += 1 + return depth +``` + +https://leetcode.cn/problems/minimum-depth-of-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/minimum-distance-between-bst-nodes 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minDiffInBST(TreeNode* root) { + traverse(root); + return res; + } + + TreeNode* prev = nullptr; + int res = INT_MAX; + + // 遍历函数 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + traverse(root->left); + // 中序遍历位置 + if (prev != nullptr) { + res = min(res, root->val - prev->val); + } + prev = root; + traverse(root->right); + } +}; +``` + +```go +// by chatGPT (go) +func minDiffInBST(root *TreeNode) int { + var ( + prev *TreeNode + res = math.MaxInt32 + // 遍历函数 + traverse func(*TreeNode) + ) + + traverse = func(root *TreeNode) { + if root == nil { + return + } + traverse(root.Left) + // 中序遍历位置 + if prev != nil { + res = min(res, root.Val - prev.Val) + } + prev = root + traverse(root.Right) + } + + traverse(root) + return res +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int minDiffInBST(TreeNode root) { + traverse(root); + return res; + } + + TreeNode prev = null; + int res = Integer.MAX_VALUE; + + // 遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.left); + // 中序遍历位置 + if (prev != null) { + res = Math.min(res, root.val - prev.val); + } + prev = root; + traverse(root.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {number} + */ +var minDiffInBST = function(root) { + let prev = null; + let res = Number.MAX_SAFE_INTEGER; + + // 遍历函数 + const traverse = (root) => { + if (!root) { + return; + } + traverse(root.left); + // 中序遍历位置 + if (prev) { + res = Math.min(res, root.val - prev.val); + } + prev = root; + traverse(root.right); + } + + traverse(root); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minDiffInBST(self, root: TreeNode) -> int: + self.prev = None + self.res = float('inf') + self.traverse(root) + return self.res + + # 遍历函数 + def traverse(self, root: TreeNode) -> None: + if not root: + return + self.traverse(root.left) + # 中序遍历位置 + if self.prev: + self.res = min(self.res, root.val - self.prev.val) + self.prev = root + self.traverse(root.right) +``` + +https://leetcode.cn/problems/minimum-distance-between-bst-nodes 的多语言解法👆 + +https://leetcode.cn/problems/minimum-falling-path-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minFallingPathSum(vector>& matrix) { + int n = matrix.size(); + int res = INT_MAX; + // 备忘录里的值初始化为 66666 + vector> memo(n, vector(n, 66666)); + // 终点可能在 matrix[n-1] 的任意一列 + for (int j = 0; j < n; j++) { + res = min(res, dp(matrix, memo, n - 1, j)); + } + return res; + } + + int dp(vector>& matrix, vector>& memo, int i, int j) { + // 1、索引合法性检查 + if (i < 0 || j < 0 || i >= matrix.size() || j >= matrix[0].size()) { + return 99999; + } + // 2、base case + if (i == 0) { + return matrix[0][j]; + } + // 3、查找备忘录,防止重复计算 + if (memo[i][j] != 66666) { + return memo[i][j]; + } + // 进行状态转移 + memo[i][j] = matrix[i][j] + three_min( + dp(matrix, memo, i - 1, j), + dp(matrix, memo, i - 1, j - 1), + dp(matrix, memo, i - 1, j + 1) + ); + + return memo[i][j]; + } + + int three_min(int a, int b, int c) { + return min(a, min(b, c)); + } +}; +``` + +```go +// by chatGPT (go) +import ( + "fmt" + "math" +) + +func minFallingPathSum(matrix [][]int) int { + n := len(matrix) + res := math.MaxInt32 + // 备忘录里的值初始化为 66666 + memo := make([][]int, n) + for i := 0; i < n; i++ { + memo[i] = make([]int, n) + for j := 0; j < n; j++ { + memo[i][j] = 66666 + } + } + // 终点可能在 matrix[n-1] 的任意一列 + for j := 0; j < n; j++ { + res = min(res, dp(matrix, n-1, j, memo)) + } + return res +} + +// 备忘录 +func dp(matrix [][]int, i, j int, memo [][]int) int { + // 1、索引合法性检查 + if i < 0 || j < 0 || + i >= len(matrix) || + j >= len(matrix[0]) { + return 99999 + } + // 2、base case + if i == 0 { + return matrix[0][j] + } + // 3、查找备忘录,防止重复计算 + if memo[i][j] != 66666 { + return memo[i][j] + } + memo[i][j] = matrix[i][j] + min( + dp(matrix, i-1, j, memo), + dp(matrix, i-1, j-1, memo), + dp(matrix, i-1, j+1, memo), + ) + return memo[i][j] +} + +// 返回三个数中的最小值 +func min(a, b, c int) int { + return int(math.Min(float64(a), math.Min(float64(b), float64(c)))) +} +``` + +```java +// by labuladong (java) +class Solution { + public int minFallingPathSum(int[][] matrix) { + int n = matrix.length; + int res = Integer.MAX_VALUE; + // 备忘录里的值初始化为 66666 + memo = new int[n][n]; + for (int i = 0; i < n; i++) { + Arrays.fill(memo[i], 66666); + } + // 终点可能在 matrix[n-1] 的任意一列 + for (int j = 0; j < n; j++) { + res = Math.min(res, dp(matrix, n - 1, j)); + } + return res; + } + + // 备忘录 + int[][] memo; + + int dp(int[][] matrix, int i, int j) { + // 1、索引合法性检查 + if (i < 0 || j < 0 || + i >= matrix.length || + j >= matrix[0].length) { + + return 99999; + } + // 2、base case + if (i == 0) { + return matrix[0][j]; + } + // 3、查找备忘录,防止重复计算 + if (memo[i][j] != 66666) { + return memo[i][j]; + } + // 进行状态转移 + memo[i][j] = matrix[i][j] + min( + dp(matrix, i - 1, j), + dp(matrix, i - 1, j - 1), + dp(matrix, i - 1, j + 1) + ); + + return memo[i][j]; + } + /** + ![](../pictures/备忘录基础/1.jpeg) + */ + int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } +} +``` + +```javascript +// by chatGPT (javascript) +var minFallingPathSum = function(matrix) { + var n = matrix.length, res = Number.MAX_VALUE; + // 备忘录里的值初始化为 66666 + var memo = new Array(n).map(() => new Array(n).fill(66666)); + // 终点可能在 matrix[n-1] 的任意一列 + for (var j = 0; j < n; j++) { + res = Math.min(res, dp(matrix, n - 1, j)); + } + return res; + + function dp(matrix, i, j) { + // 1、索引合法性检查 + if (i < 0 || j < 0 || i >= matrix.length || j >= matrix[0].length) { + return 99999; + } + // 2、base case + if (i === 0) { + return matrix[0][j]; + } + // 3、查找备忘录,防止重复计算 + if (memo[i][j] !== 66666) { + return memo[i][j]; + } + // 进行状态转移 + memo[i][j] = matrix[i][j] + min( + dp(matrix, i - 1, j), + dp(matrix, i - 1, j - 1), + dp(matrix, i - 1, j + 1) + ); + return memo[i][j]; + } + + // 求三者最小值 + function min(a, b, c) { + return Math.min(a, Math.min(b, c)); + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minFallingPathSum(self, matrix: List[List[int]]) -> int: + n = len(matrix) + res = float('inf') + # 备忘录里的值初始化为 66666 + memo = [[66666]*n for _ in range(n)] + # 终点可能在 matrix[n-1] 的任意一列 + for j in range(n): + res = min(res, self.dp(matrix, memo, n-1, j)) + return res + + def dp(self, matrix, memo, i, j): + # 1. 索引合法性检查 + if i < 0 or j < 0 or i >= len(matrix) or j >= len(matrix[0]): + return 99999 + # 2. base case + if i == 0: + return matrix[0][j] + # 3. 查找备忘录,防止重复计算 + if memo[i][j] != 66666: + return memo[i][j] + # 进行状态转移 + memo[i][j] = matrix[i][j] + min( + self.dp(matrix, memo, i-1, j), + self.dp(matrix, memo, i-1, j-1), + self.dp(matrix, memo, i-1, j+1) + ) + return memo[i][j] +``` + +https://leetcode.cn/problems/minimum-falling-path-sum 的多语言解法👆 + +https://leetcode.cn/problems/minimum-height-trees 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector findMinHeightTrees(int n, vector>& edges) { + // 1、构建邻接表 + vector> graph(n); + for (auto& edge : edges) { + // 无向图,等同于双向图 + graph[edge[0]].push_back(edge[1]); + graph[edge[1]].push_back(edge[0]); + } + + // 2、找到所有的叶子节点 + vector leaves; + for (int i = 0; i < n; i++) { + if (graph[i].size() == 1) { + leaves.push_back(i); + } + } + + // 3、不断删除叶子节点,直到剩下的节点数小于等于 2 个 + int remainNodeNum = n; + while (remainNodeNum > 2) { + // 删除当前叶子节点,计算新的叶子节点 + remainNodeNum -= leaves.size(); + vector newLeaves; + for (auto leaf : leaves) { + // 将被删除的叶子节点的邻接节点的度减 1 + int neighbor = graph[leaf][0]; + graph[neighbor].erase(find(graph[neighbor].begin(), graph[neighbor].end(), leaf)); + // 如果邻接节点的度为 1,说明它也变成了叶子节点 + if (graph[neighbor].size() == 1) { + newLeaves.push_back(neighbor); + } + } + leaves = newLeaves; + } + + // 最后剩下的节点就是根节点 + return leaves; + } +}; +``` + +```go +// by chatGPT (go) +func findMinHeightTrees(n int, edges [][]int) []int { + // 1、构建邻接表 + graph := make([][]int, n) + for i := 0; i < n; i++ { + graph[i] = []int{} + } + for _, edge := range edges { + // 无向图,等同于双向图 + graph[edge[0]] = append(graph[edge[0]], edge[1]) + graph[edge[1]] = append(graph[edge[1]], edge[0]) + } + + // 2、找到所有的叶子节点 + leaves := []int{} + for i := 0; i < n; i++ { + if len(graph[i]) == 1 { + leaves = append(leaves, i) + } + } + + // 3、不断删除叶子节点,直到剩下的节点数小于等于 2 个 + remainNodeNum := n + for remainNodeNum > 2 { + // 删除当前叶子节点,计算新的叶子节点 + remainNodeNum -= len(leaves) + newLeaves := []int{} + for _, leaf := range leaves { + // 将被删除的叶子节点的邻接节点的度减 1 + neighbor := graph[leaf][0] + graph[neighbor] = removeElement(graph[neighbor], leaf) + // 如果邻接节点的度为 1,说明它也变成了叶子节点 + if len(graph[neighbor]) == 1 { + newLeaves = append(newLeaves, neighbor) + } + } + leaves = newLeaves + } + + // 最后剩下的节点就是根节点 + return leaves +} + +// 删除切片中的元素 +func removeElement(slice []int, elem int) []int { + index := -1 + for i, v := range slice { + if v == elem { + index = i + break + } + } + if index == -1 { + return slice + } + return append(slice[:index], slice[index+1:]...) +} +``` + +```java +// by labuladong (java) +class Solution { + public List findMinHeightTrees(int n, int[][] edges) { + // 1、构建邻接表 + List> graph = new ArrayList<>(); + for (int i = 0; i < n; i++) { + graph.add(new ArrayList()); + } + for (int[] edge : edges) { + // 无向图,等同于双向图 + graph.get(edge[0]).add(edge[1]); + graph.get(edge[1]).add(edge[0]); + } + + // 2、找到所有的叶子节点 + List leaves = new ArrayList<>(); + for (int i = 0; i < n; i++) { + if (graph.get(i).size() == 1) { + leaves.add(i); + } + } + + // 3、不断删除叶子节点,直到剩下的节点数小于等于 2 个 + int remainNodeNum = n; + while (remainNodeNum > 2) { + // 删除当前叶子节点,计算新的叶子节点 + remainNodeNum -= leaves.size(); + List newLeaves = new ArrayList<>(); + for (int leaf : leaves) { + // 将被删除的叶子节点的邻接节点的度减 1 + int neighbor = graph.get(leaf).get(0); + graph.get(neighbor).remove(leaf); + // 如果邻接节点的度为 1,说明它也变成了叶子节点 + if (graph.get(neighbor).size() == 1) { + newLeaves.add(neighbor); + } + } + + // 最后剩下的节点就是根节点 + return leaves; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findMinHeightTrees = function(n, edges) { + // 1、构建邻接表 + let graph = new Array(n); + for(let i = 0; i < n; i++) { + graph[i] = []; + } + for(let edge of edges) { + // 无向图,等同于双向图 + graph[edge[0]].push(edge[1]); + graph[edge[1]].push(edge[0]); + } + + // 2、找到所有的叶子节点 + let leaves = []; + for(let i = 0; i < n; i++) { + if(graph[i].length === 1) { + leaves.push(i); + } + } + + // 3、不断删除叶子节点,直到剩下的节点数小于等于 2 个 + let remainNodeNum = n; + while(remainNodeNum > 2) { + // 删除当前叶子节点,计算新的叶子节点 + remainNodeNum -= leaves.length; + let newLeaves = []; + for(let leaf of leaves) { + // 将被删除的叶子节点的邻接节点的度减 1 + let neighbor = graph[leaf][0]; + graph[neighbor].splice(graph[neighbor].indexOf(leaf), 1); + // 如果邻接节点的度为 1,说明它也变成了叶子节点 + if(graph[neighbor].length === 1) { + newLeaves.push(neighbor); + } + } + leaves = newLeaves; + } + + // 最后剩下的节点就是根节点 + return leaves; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]: + # 1、构建邻接表 + graph = [[] for _ in range(n)] + for edge in edges: + # 无向图,等同于双向图 + graph[edge[0]].append(edge[1]) + graph[edge[1]].append(edge[0]) + + # 2、找到所有的叶子节点 + leaves = [] + for i in range(n): + if len(graph[i]) == 1: + leaves.append(i) + + # 3、不断删除叶子节点,直到剩下的节点数小于等于 2 个 + remainNodeNum = n + while remainNodeNum > 2: + # 删除当前叶子节点,计算新的叶子节点 + remainNodeNum -= len(leaves) + newLeaves = [] + for leaf in leaves: + # 将被删除的叶子节点的邻接节点的度减 1 + neighbor = graph[leaf][0] + graph[neighbor].remove(leaf) + # 如果邻接节点的度为 1,说明它也变成了叶子节点 + if len(graph[neighbor]) == 1: + newLeaves.append(neighbor) + + leaves = newLeaves + + # 最后剩下的节点就是根节点 + return leaves +``` + +https://leetcode.cn/problems/minimum-height-trees 的多语言解法👆 + +https://leetcode.cn/problems/minimum-insertions-to-balance-a-parentheses-string 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minInsertions(string s) { + int res = 0, need = 0; + + for (int i = 0; i < s.length(); i++) { + if (s[i] == '(') { + need += 2; + if (need % 2 == 1) { + res++; + need--; + } + } + + if (s[i] == ')') { + need--; + if (need == -1) { + res++; + need = 1; + } + } + } + + return res + need; + } +}; +``` + +```go +// by chatGPT (go) +func minInsertions(s string) int { + res := 0 + need := 0 + + for i := 0; i < len(s); i++ { + if s[i] == '(' { + need += 2 + if need%2 == 1 { + res++ + need-- + } + } else if s[i] == ')' { + need-- + if need == -1 { + res++ + need = 1 + } + } + } + + return res + need +} +``` + +```java +// by labuladong (java) +class Solution { + public int minInsertions(String s) { + int res = 0, need = 0; + + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + need += 2; + if (need % 2 == 1) { + res++; + need--; + } + } + + if (s.charAt(i) == ')') { + need--; + if (need == -1) { + res++; + need = 1; + } + } + } + + return res + need; + } +} +``` + +```javascript +// by chatGPT (javascript) +var minInsertions = function(s) { + let res = 0, need = 0; + + for (let i = 0; i < s.length; i++) { + if (s.charAt(i) == '(') { + need += 2; + if (need % 2 == 1) { + res++; + need--; + } + } + + if (s.charAt(i) == ')') { + need--; + if (need == -1) { + res++; + need = 1; + } + } + } + return res + need; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minInsertions(self, s: str) -> int: + res = 0 + need = 0 + + # Traverse through string s + for i in range(len(s)): + if s[i] == '(': + need += 2 + if need % 2 == 1: + res += 1 + need -= 1 + + if s[i] == ')': + need -= 1 + if need == -1: + res += 1 + need = 1 + + return res + need +``` + +https://leetcode.cn/problems/minimum-insertions-to-balance-a-parentheses-string 的多语言解法👆 + +https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 区间调度问题 + int findMinArrowShots(vector>& intvs) { + if (intvs.size() == 0) return 0; + // 按 end 升序排序 + sort(intvs.begin(), intvs.end(), [](const auto& a, const auto& b) { + return a[1] < b[1]; + }); + // 至少有一个区间不相交 + int count = 1; + // 排序后,第一个区间就是 x + int x_end = intvs[0][1]; + for (auto& interval : intvs) { + int start = interval[0]; + // 把 >= 改成 > 就行了 + if (start > x_end) { + count++; + x_end = interval[1]; + } + } + return count; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "fmt" + "sort" +) + +// findMinArrowShots 是区间调度问题的解决方案 +func findMinArrowShots(intvs [][]int) int { + if len(intvs) == 0 { + return 0 + } + // 按 end 升序排序 + sort.Slice(intvs, func(i, j int) bool { + return intvs[i][1] < intvs[j][1] + }) + // 至少有一个区间不相交 + count := 1 + // 排序后,第一个区间就是 x + xEnd := intvs[0][1] + for _, interval := range intvs { + start := interval[0] + // 把 >= 改成 > 就行了 + if start > xEnd { + count++ + xEnd = interval[1] + } + } + return count +} +``` + +```java +// by labuladong (java) +class Solution { + // 区间调度问题 + public int findMinArrowShots(int[][] intvs) { + if (intvs.length == 0) return 0; + // 按 end 升序排序 + Arrays.sort(intvs, new Comparator() { + public int compare(int[] a, int[] b) { + return a[1] - b[1]; + } + }); + // 至少有一个区间不相交 + int count = 1; + // 排序后,第一个区间就是 x + int x_end = intvs[0][1]; + for (int[] interval : intvs) { + int start = interval[0]; + // 把 >= 改成 > 就行了 + if (start > x_end) { + count++; + x_end = interval[1]; + } + } + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} intvs + * @return {number} + */ +var findMinArrowShots = function(intvs) { + if (intvs.length == 0) return 0; + // 按 end 升序排序 + intvs.sort((a, b) => { return a[1] - b[1]; }); + // 至少有一个区间不相交 + let count = 1; + // 排序后,第一个区间就是 x + let xEnd = intvs[0][1]; + for (let interval of intvs) { + let start = interval[0]; + // 把 >= 改成 > 就行了 + if (start > xEnd) { + count++; + xEnd = interval[1]; + } + } + return count; +} +``` + +```python +# by chatGPT (python) +# 区间调度问题 +class Solution: + def findMinArrowShots(self, intvs: List[List[int]]) -> int: + if not intvs: + return 0 + # 按 end 升序排序 + intvs.sort(key=lambda x: x[1]) + # 至少有一个区间不相交 + count = 1 + # 排序后,第一个区间就是 x + x_end = intvs[0][1] + for interval in intvs: + start = interval[0] + # 把 >= 改成 > 就行了 + if start > x_end: + count += 1 + x_end = interval[1] + return count +``` + +https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons 的多语言解法👆 + +https://leetcode.cn/problems/minimum-path-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + int memo[205][205]; + +public: + int minPathSum(vector>& grid) { + int m = grid.size(); + int n = grid[0].size(); + memset(memo, -1, sizeof memo); + + return dp(grid, m - 1, n - 1); + } + + int dp(vector>& grid, int i, int j) { + // base case + if (i == 0 && j == 0) { + return grid[0][0]; + } + if (i < 0 || j < 0) { + return INT_MAX; + } + + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + + // 将计算结果记入备忘录 + memo[i][j] = min( + dp(grid, i - 1, j), + dp(grid, i, j - 1) + ) + grid[i][j]; + + return memo[i][j]; + } +}; +``` + +```go +// by chatGPT (go) +import "math" + +func minPathSum(grid [][]int) int { + m, n := len(grid), len(grid[0]) + // 构造备忘录,初始值全部设为 -1 + memo := make([][]int, m) + for i := range memo { + memo[i] = make([]int, n) + for j := range memo[i] { + memo[i][j] = -1 + } + } + + var dp func(i, j int) int + dp = func(i, j int) int { + // base case + if i == 0 && j == 0 { + return grid[0][0] + } + if i < 0 || j < 0 { + return int(math.MaxInt64) + } + // 避免重复计算 + if memo[i][j] != -1 { + return memo[i][j] + } + // 将计算结果记入备忘录 + memo[i][j] = min( + dp(i-1, j), + dp(i, j-1), + ) + grid[i][j] + + return memo[i][j] + } + return dp(m-1, n-1) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + int[][] memo; + + public int minPathSum(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + // 构造备忘录,初始值全部设为 -1 + memo = new int[m][n]; + for (int[] row : memo) + Arrays.fill(row, -1); + + return dp(grid, m - 1, n - 1); + } + + int dp(int[][] grid, int i, int j) { + // base case + if (i == 0 && j == 0) { + return grid[0][0]; + } + if (i < 0 || j < 0) { + return Integer.MAX_VALUE; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 将计算结果记入备忘录 + memo[i][j] = Math.min( + dp(grid, i - 1, j), + dp(grid, i, j - 1) + ) + grid[i][j]; + + return memo[i][j]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var minPathSum = function(grid) { + const m = grid.length; + const n = grid[0].length; + // 构造备忘录,初始值全部设为 -1 + const memo = new Array(m); + for (let i = 0; i < m; i++) { + memo[i] = new Array(n).fill(-1); + } + + return dp(grid, m - 1, n - 1); + + function dp(grid, i, j) { + if (i === 0 && j === 0) { + // base case + return grid[0][0]; + } + if (i < 0 || j < 0) { + // 越界 + return Infinity; + } + // 避免重复计算 + if (memo[i][j] !== -1) { + return memo[i][j]; + } + // 将计算结果记入备忘录 + memo[i][j] = Math.min( + dp(grid, i - 1, j), + dp(grid, i, j - 1) + ) + grid[i][j]; + + return memo[i][j]; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minPathSum(self, grid: List[List[int]]) -> int: + m = len(grid) + n = len(grid[0]) + # 构造备忘录,初始值全部设为 -1 + memo = [[-1 for _ in range(n)] for _ in range(m)] + + return self.dp(grid, m - 1, n - 1, memo) + + def dp(self, grid: List[List[int]], i: int, j: int, memo: List[List[int]]) -> int: + # base case + if i == 0 and j == 0: + return grid[0][0] + if i < 0 or j < 0: + return float("inf") + # 避免重复计算 + if memo[i][j] != -1: + return memo[i][j] + # 将计算结果记入备忘录 + memo[i][j] = min( + self.dp(grid, i - 1, j, memo), + self.dp(grid, i, j - 1, memo) + ) + grid[i][j] + + return memo[i][j] +``` + +https://leetcode.cn/problems/minimum-path-sum 的多语言解法👆 + +https://leetcode.cn/problems/minimum-window-substring 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + string minWindow(string s, string t) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + // 记录最小覆盖子串的起始索引及长度 + int start = 0, len = INT_MAX; + /** + ![](../pictures/slidingwindow/1.png) + */ + while (right < s.size()) { + // c 是将移入窗口的字符 + char c = s[right]; + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + if (need.count(c)) { + window[c]++; + if (window[c] == need[c]) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (valid == need.size()) { + /** + ![](../pictures/slidingwindow/2.png) + */ + // 在这里更新最小覆盖子串 + if (right - left < len) { + start = left; + len = right - left; + } + // d 是将移出窗口的字符 + char d = s[left]; + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + if (need.count(d)) { + if (window[d] == need[d]) + valid--; + window[d]--; + } + } + /** + ![](../pictures/slidingwindow/4.png) + */ + } + // 返回最小覆盖子串 + return len == INT_MAX ? + "" : s.substr(start, len); + } +}; +``` + +```go +// by mario_huang (go) +func minWindow(s string, t string) string { + need := map[byte]int{} + window := map[byte]int{} + for _, c := range []byte(t) { + need[c]++ + } + left, right := 0, 0 + valid := 0 + // 记录最小覆盖子串的起始索引及长度 + start, length := 0, math.MaxInt + /** + ![](../pictures/slidingwindow/1.png) + */ + for right < len(s) { + // c 是将移入窗口的字符 + c := s[right] + // 右移窗口 + right++ + // 进行窗口内数据的一系列更新 + if _, ok := need[c]; ok { + window[c]++ + if window[c] == need[c] { + valid++ + } + } + // 判断左侧窗口是否要收缩 + for valid == len(need) { + /** + ![](../pictures/slidingwindow/2.png) + */ + // 在这里更新最小覆盖子串 + if right-left < length { + start = left + length = right - left + } + // d 是将移出窗口的字符 + d := s[left] + // 左移窗口 + left++ + // 进行窗口内数据的一系列更新 + if _, ok := need[d]; ok { + if window[d] == need[d] { + valid-- + } + window[d]-- + } + } + /** + ![](../pictures/slidingwindow/4.png) + */ + } + // 返回最小覆盖子串 + if length == math.MaxInt { + return "" + } else { + return s[start : start+length] + } +} +``` + +```java +// by chatGPT (java) +class Solution { + public String minWindow(String s, String t) { + Map need = new HashMap<>(); + Map window = new HashMap<>(); + for (char c : t.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1); + + int left = 0, right = 0; + int valid = 0; + // 记录最小覆盖子串的起始索引及长度 + int start = 0, len = Integer.MAX_VALUE; + + while (right < s.length()) { + // c 是将移入窗口的字符 + char c = s.charAt(right); + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (valid == need.size()) { + // 在这里更新最小覆盖子串 + if (right - left < len) { + start = left; + len = right - left; + } + // d 是将移出窗口的字符 + char d = s.charAt(left); + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) + valid--; + window.put(d, window.get(d) - 1); + } + } + } + + // 返回最小覆盖子串 + return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len); + } +} +``` + +```javascript +// by chatGPT (javascript) +var minWindow = function(s, t) { + const need = new Map(); + const window = new Map(); + // 统计 t 中每个字符出现的次数 + for (let c of t) { + need.set(c, need.has(c) ? need.get(c) + 1 : 1) + } + + let left = 0, right = 0; + let valid = 0; + // 记录最小覆盖子串的起始索引及长度 + let start = 0, len = Infinity; + while (right < s.length) { + // c 是将移入窗口的字符 + let c = s[right]; + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + if (need.has(c)) { + window.set(c, window.has(c) ? window.get(c) + 1 : 1) + if (window.get(c) === need.get(c)) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (valid === need.size) { + // 在这里更新最小覆盖子串 + if (right - left < len) { + start = left; + len = right - left; + } + // d 是将移出窗口的字符 + let d = s[left]; + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + if (need.has(d)) { + if (window.get(d) === need.get(d)) + valid--; + window.set(d, window.get(d) - 1) + } + } + } + // 返回最小覆盖子串 + return len === Infinity ? + "" : s.substr(start, len); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def minWindow(self, s: str, t: str) -> str: + from collections import Counter + need = Counter(t) + window = Counter() + + left, right = 0, 0 + valid = 0 + # 记录最小覆盖子串的起始索引及长度 + start, length = 0, float('inf') + """ + + ![](../pictures/slidingwindow/1.png) + """ + while right < len(s): + # c 是将移入窗口的字符 + c = s[right] + right += 1 + # 进行窗口内数据的一系列更新 + if c in need: + window[c] += 1 + if window[c] == need[c]: + valid += 1 + + # 判断左侧窗口是否要收缩 + while valid == len(need): + """ + + ![](../pictures/slidingwindow/2.png) + """ + # 在这里更新最小覆盖子串 + if right - left < length: + start = left + length = right - left + # d 是将移出窗口的字符 + d = s[left] + left += 1 + # 进行窗口内数据的一系列更新 + if d in need: + if window[d] == need[d]: + valid -= 1 + window[d] -= 1 + + """ + + ![](../pictures/slidingwindow/4.png) + """ + # 返回最小覆盖子串 + return '' if length == float('inf') else s[start:start+length] +``` + +https://leetcode.cn/problems/minimum-window-substring 的多语言解法👆 + +https://leetcode.cn/problems/missing-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int missingNumber(vector& nums) { + int n = nums.size(); + int res = 0; + // 先和新补的索引异或一下 + res ^= n; + // 和其他的元素、索引做异或 + for (int i = 0; i < n; i++) + res ^= i ^ nums[i]; + return res; + } +}; +``` + +```go +// by chatGPT (go) +/* + * @lc app=leetcode id=268 lang=golang + * + * [268] Missing Number + * + * https://leetcode.com/problems/missing-number/description/ + * + * algorithms + * Easy (49.34%) + * Total Accepted: 321.9K + * Total Submissions: 647.5K + * Testcase Example: '[3,0,1]' + * + * Given an array containing n distinct numbers taken from 0, 1, 2, ..., n, + * find the one that is missing from the array. + * + * Example 1: + * + * + * Input: [3,0,1] + * Output: 2 + * + * + * Example 2: + * + * + * Input: [9,6,4,2,3,5,7,0,1] + * Output: 8 + * + * + * Note: + * Your algorithm should run in linear runtime complexity. Could you implement + * it using only constant extra space complexity? + */ + +func missingNumber(nums []int) int { + n := len(nums) + res := 0 + // 先和新补的索引异或一下 + res ^= n + // 和其他的元素、索引做异或 + for i := 0; i < n; i++ { + res ^= i ^ nums[i] + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int missingNumber(int[] nums) { + int n = nums.length; + int res = 0; + // 先和新补的索引异或一下 + res ^= n; + // 和其他的元素、索引做异或 + for (int i = 0; i < n; i++) + res ^= i ^ nums[i]; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var missingNumber = function(nums) { + let n = nums.length; + let res = 0; + // 先和新补的索引异或一下 + res ^= n; + // 和其他的元素、索引做异或 + for (let i = 0; i < n; i++) + res ^= i ^ nums[i]; + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def missingNumber(self, nums: List[int]) -> int: + n = len(nums) + res = 0 + # 先和新补的索引异或一下 + res ^= n + # 和其他的元素、索引做异或 + for i in range(n): + res ^= i ^ nums[i] + return res +``` + +https://leetcode.cn/problems/missing-number 的多语言解法👆 + +https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int removeStones(vector>& stones) { + int n = stones.size(); + + // 一维坐标 -> 节点 ID + unordered_map codeToId; + for (int i = 0; i < n; i++) { + codeToId[encode(stones[i])] = i; + } + + // 记录每一行每一列有哪些节点 + unordered_map> colIndexToCodes, rowIndexToCodes; + for (auto point : stones) { + int x = point[0], y = point[1]; + rowIndexToCodes[x].push_back(encode(point)); + colIndexToCodes[y].push_back(encode(point)); + } + + // 启动 union find 算法 + UF uf(n); + + // 把每一列的节点连通 + for (auto& [index, col] : colIndexToCodes) { + int firstId = codeToId[col[0]]; + for (int i = 1; i < col.size(); i++) { + int otherId = codeToId[col[i]]; + uf.my_union(firstId, otherId); + } + } + + // 把每一行的节点连通 + for (auto& [index, row] : rowIndexToCodes) { + int firstId = codeToId[row[0]]; + for (int i = 1; i < row.size(); i++) { + int otherId = codeToId[row[i]]; + uf.my_union(firstId, otherId); + } + } + // 石头总数 - 连通分量数量就是被消除的石头个数 + return n - uf.count(); + } + + // 将二维坐标转化成一维索引 + int encode(vector& point) { + return point[0] * 10000 + point[1]; + } + +}; + +// union find 算法模板 +class UF { +public: + // 连通分量个数 + int count; + // 存储每个节点的父节点 + vector parent; + + // n 为图中节点的个数 + UF(int n) : count(n), parent(n) { + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + // 将节点 p 和节点 q 连通 + void my_union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + + if (rootP == rootQ) { + return; + } + + parent[rootQ] = rootP; + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + bool connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } +}; +``` + +```go +// by chatGPT (go) +func removeStones(stones [][]int) int { + n := len(stones) + + // 一维坐标 -> 节点 ID + codeToId := make(map[int]int, n) + for i := 0; i < n; i++ { + codeToId[encode(stones[i])] = i + } + + // 记录每一行每一列有哪些节点 + colIndexToCodes := make(map[int][]int) + rowIndexToCodes := make(map[int][]int) + for _, point := range stones { + x, y := point[0], point[1] + rowIndexToCodes[x] = append(rowIndexToCodes[x], encode(point)) + colIndexToCodes[y] = append(colIndexToCodes[y], encode(point)) + } + + // 启动 union find 算法 + uf := newUF(n) + + // 把每一列的节点连通 + for _, col := range colIndexToCodes { + firstId := codeToId[col[0]] + for i := 1; i < len(col); i++ { + otherId := codeToId[col[i]] + uf.union(firstId, otherId) + } + } + + // 把每一行的节点连通 + for _, row := range rowIndexToCodes { + firstId := codeToId[row[0]] + for i := 1; i < len(row); i++ { + otherId := codeToId[row[i]] + uf.union(firstId, otherId) + } + } + // 石头总数 - 连通分量数量就是被消除的石头个数 + return n - uf.count() +} + +// 将二维坐标转化成一维索引 +func encode(point []int) int { + return point[0]*10000 + point[1] +} + +type UF struct { + // 连通分量个数 + count int + // 存储每个节点的父节点 + parent []int +} + +// n 为图中节点的个数 +func newUF(n int) *UF { + uf := new(UF) + uf.count = n + uf.parent = make([]int, n) + for i := 0; i < n; i++ { + uf.parent[i] = i + } + return uf +} + +// 将节点 p 和节点 q 连通 +func (uf *UF) union(p int, q int) { + rootP := uf.find(p) + rootQ := uf.find(q) + + if rootP == rootQ { + return + } + + uf.parent[rootQ] = rootP + // 两个连通分量合并成一个连通分量 + uf.count-- +} + +// 判断节点 p 和节点 q 是否连通 +func (uf *UF) connected(p int, q int) bool { + rootP := uf.find(p) + rootQ := uf.find(q) + return rootP == rootQ +} + +func (uf *UF) find(x int) int { + if uf.parent[x] != x { + uf.parent[x] = uf.find(uf.parent[x]) + } + return uf.parent[x] +} + +// 返回图中的连通分量个数 +func (uf *UF) count() int { + return uf.count +} +``` + +```java +// by labuladong (java) +class Solution { + public int removeStones(int[][] stones) { + int n = stones.length; + + // 一维坐标 -> 节点 ID + HashMap codeToId = new HashMap<>(); + for (int i = 0; i < n; i++) { + codeToId.put(encode(stones[i]), i); + } + + // 记录每一行每一列有哪些节点 + HashMap> colIndexToCodes = new HashMap<>(); + HashMap> rowIndexToCodes = new HashMap<>(); + for (int[] point : stones) { + int x = point[0], y = point[1]; + rowIndexToCodes.putIfAbsent(x, new ArrayList<>()); + colIndexToCodes.putIfAbsent(y, new ArrayList<>()); + rowIndexToCodes.get(x).add(encode(point)); + colIndexToCodes.get(y).add(encode(point)); + } + + // 启动 union find 算法 + UF uf = new UF(n); + + // 把每一列的节点连通 + for (int index : colIndexToCodes.keySet()) { + List col = colIndexToCodes.get(index); + int firstId = codeToId.get(col.get(0)); + for (int i = 1; i < col.size(); i++) { + int otherId = codeToId.get(col.get(i)); + uf.union(firstId, otherId); + } + } + + // 把每一行的节点连通 + for (int index : rowIndexToCodes.keySet()) { + List row = rowIndexToCodes.get(index); + int firstId = codeToId.get(row.get(0)); + for (int i = 1; i < row.size(); i++) { + int otherId = codeToId.get(row.get(i)); + uf.union(firstId, otherId); + } + } + // 石头总数 - 连通分量数量就是被消除的石头个数 + return n - uf.count(); + } + + // 将二维坐标转化成一维索引 + int encode(int[] point) { + return point[0] * 10000 + point[1]; + } + +} + +// union find 算法模板 +class UF { + // 连通分量个数 + private int count; + // 存储每个节点的父节点 + private int[] parent; + + // n 为图中节点的个数 + public UF(int n) { + this.count = n; + parent = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + // 将节点 p 和节点 q 连通 + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + + if (rootP == rootQ) { + return; + } + + parent[rootQ] = rootP; + // 两个连通分量合并成一个连通分量 + count--; + } + + // 判断节点 p 和节点 q 是否连通 + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } + + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } + + // 返回图中的连通分量个数 + public int count() { + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} stones + * @return {number} + */ +var removeStones = function(stones) { + const n = stones.length; + + // 一维坐标 -> 节点 ID + let codeToId = new Map(); + for (let i = 0; i < n; i++) { + codeToId.set(encode(stones[i]), i); + } + + // 记录每一行每一列有哪些节点 + let colIndexToCodes = new Map(); + let rowIndexToCodes = new Map(); + stones.forEach(point => { + const x = point[0], y = point[1]; + rowIndexToCodes.set(x, rowIndexToCodes.get(x) || []); + colIndexToCodes.set(y, colIndexToCodes.get(y) || []); + rowIndexToCodes.get(x).push(encode(point)); + colIndexToCodes.get(y).push(encode(point)); + }); + + // 启动 union find 算法 + let uf = new UF(n); + + // 把每一列的节点连通 + for (let index of colIndexToCodes.keys()) { + const col = colIndexToCodes.get(index); + const firstId = codeToId.get(col[0]); + for (let i = 1; i < col.length; i++) { + const otherId = codeToId.get(col[i]); + uf.union(firstId, otherId); + } + } + + // 把每一行的节点连通 + for (let index of rowIndexToCodes.keys()) { + const row = rowIndexToCodes.get(index); + const firstId = codeToId.get(row[0]); + for (let i = 1; i < row.length; i++) { + const otherId = codeToId.get(row[i]); + uf.union(firstId, otherId); + } + } + + // 石头总数 - 连通分量数量就是被消除的石头个数 + return n - uf.count(); +}; + +// 将二维坐标转化成一维索引 +function encode(point) { + return point[0] * 10000 + point[1]; +} + +// union find 算法模板 +class UF { + // 连通分量个数 + count = 0; + // 存储每个节点的父节点 + parent = []; + + // n 为图中节点的个数 + constructor(n) { + this.count = n; + this.parent = new Array(n).fill(0); + for (let i = 0; i < n; i++) { + this.parent[i] = i; + } + } + + // 将节点 p 和节点 q 连通 + union(p, q) { + let rootP = this.find(p); + let rootQ = this.find(q); + + if (rootP === rootQ) { + return; + } + + this.parent[rootQ] = rootP; + // 两个连通分量合并成一个连通分量 + this.count--; + } + + // 判断节点 p 和节点 q 是否连通 + connected(p, q) { + let rootP = this.find(p); + let rootQ = this.find(q); + return rootP === rootQ; + } + + find(x) { + if (this.parent[x] !== x) { + this.parent[x] = this.find(this.parent[x]); + } + return this.parent[x]; + } + + // 返回图中的连通分量个数 + count() { + return this.count; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def removeStones(self, stones: List[List[int]]) -> int: + n = len(stones) + + # 一维坐标 -> 节点 ID + codeToId = {} + for i in range(n): + codeToId[self.encode(stones[i])] = i + + # 记录每一行每一列有哪些节点 + colIndexToCodes = {} + rowIndexToCodes = {} + for point in stones: + x, y = point[0], point[1] + rowIndexToCodes.setdefault(x, []) + colIndexToCodes.setdefault(y, []) + rowIndexToCodes[x].append(self.encode(point)) + colIndexToCodes[y].append(self.encode(point)) + + # 启动 union find 算法 + uf = UF(n) + + # 把每一列的节点连通 + for index in colIndexToCodes.keys(): + col = colIndexToCodes[index] + firstId = codeToId[col[0]] + for i in range(1, len(col)): + otherId = codeToId[col[i]] + uf.union(firstId, otherId) + + # 把每一行的节点连通 + for index in rowIndexToCodes.keys(): + row = rowIndexToCodes[index] + firstId = codeToId[row[0]] + for i in range(1, len(row)): + otherId = codeToId[row[i]] + uf.union(firstId, otherId) + + # 石头总数 - 连通分量数量就是被消除的石头个数 + return n - uf.count() + + # 将二维坐标转化成一维索引 + def encode(self, point: List[int]) -> int: + return point[0] * 10000 + point[1] + + +# union find 算法模板 +class UF: + # 连通分量个数 + def __init__(self, n: int): + # 存储每个节点的父节点 + self.parent = list(range(n)) + self.count = n + + # 将节点 p 和节点 q 连通 + def union(self, p: int, q: int) -> None: + rootP = self.find(p) + rootQ = self.find(q) + + if rootP == rootQ: + return + + self.parent[rootQ] = rootP + # 两个连通分量合并成一个连通分量 + self.count -= 1 + + # 判断节点 p 和节点 q 是否连通 + def connected(self, p: int, q: int) -> bool: + rootP = self.find(p) + rootQ = self.find(q) + return rootP == rootQ + + def find(self, x: int) -> int: + if self.parent[x] != x: + self.parent[x] = self.find(self.parent[x]) + return self.parent[x] + + # 返回图中的连通分量个数 + def count(self) -> int: + return self.count +``` + +https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column 的多语言解法👆 + +https://leetcode.cn/problems/move-zeroes 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + void moveZeroes(vector& nums) { + // 去除 nums 中的所有 0 + // 返回去除 0 之后的数组长度 + int p = removeElement(nums, 0); + // 将 p 之后的所有元素赋值为 0 + for (; p < nums.size(); p++) { + nums[p] = 0; + } + } + + // 双指针技巧,复用 [27. 移除元素] 的解法。 + int removeElement(vector& nums, int val) { + int fast = 0, slow = 0; + while (fast < nums.size()) { + if (nums[fast] != val) { + nums[slow] = nums[fast]; + slow++; + } + fast++; + } + return slow; + } +}; +``` + +```go +// by chatGPT (go) +func moveZeroes(nums []int) { + // 去除 nums 中的所有 0 + // 返回去除 0 之后的数组长度 + p := removeElement(nums, 0) + // 将 p 之后的所有元素赋值为 0 + for ; p < len(nums); p++ { + nums[p] = 0 + } +} + +// 双指针技巧,复用 [27. 移除元素] 的解法。 +func removeElement(nums []int, val int) int { + fast := 0 + slow := 0 + for fast < len(nums) { + if nums[fast] != val { + nums[slow] = nums[fast] + slow++ + } + fast++ + } + return slow +} +``` + +```java +// by labuladong (java) +class Solution { + public void moveZeroes(int[] nums) { + // 去除 nums 中的所有 0 + // 返回去除 0 之后的数组长度 + int p = removeElement(nums, 0); + // 将 p 之后的所有元素赋值为 0 + for (; p < nums.length; p++) { + nums[p] = 0; + } + } + + // 双指针技巧,复用 [27. 移除元素] 的解法。 + int removeElement(int[] nums, int val) { + int fast = 0, slow = 0; + while (fast < nums.length) { + if (nums[fast] != val) { + nums[slow] = nums[fast]; + slow++; + } + fast++; + } + return slow; + } +} +``` + +```javascript +// by chatGPT (javascript) +var moveZeroes = function(nums) { + // 去除 nums 中的所有 0 + // 返回去除 0 之后的数组长度 + var p = removeElement(nums, 0); + // 将 p 之后的所有元素赋值为 0 + for (; p < nums.length; p++) { + nums[p] = 0; + } +}; + +// 双指针技巧,复用 [27. 移除元素] 的解法。 +var removeElement = function(nums, val) { + var fast = 0, slow = 0; + while (fast < nums.length) { + if (nums[fast] !== val) { + nums[slow] = nums[fast]; + slow++; + } + fast++; + } + return slow; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def moveZeroes(self, nums: List[int]) -> None: + """ + Do not return anything, modify nums in-place instead. + """ + # 去除 nums 中的所有 0 + # 返回去除 0 之后的数组长度 + p = self.removeElement(nums, 0) + # 将 p 之后的所有元素赋值为 0 + for i in range(p, len(nums)): + nums[i] = 0 + + # 双指针技巧,复用 [27. 移除元素] 的解法。 + def removeElement(self, nums: List[int], val: int) -> int: + fast = 0 + slow = 0 + while fast < len(nums): + if nums[fast] != val: + nums[slow] = nums[fast] + slow += 1 + fast += 1 + return slow +``` + +https://leetcode.cn/problems/move-zeroes 的多语言解法👆 + +https://leetcode.cn/problems/n-ary-tree-level-order-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> levelOrder(Node* root) { + vector> res; + if (root == nullptr) { + return res; + } + + // 以下是标准的 BFS 算法遍历框架 + queue q; + q.push(root); + + while (!q.empty()) { + int sz = q.size(); + vector level; + for (int i = 0; i < sz; i++) { + Node* cur = q.front(); + q.pop(); + level.push_back(cur->val); + // 多叉树可能有多个子节点,将所有子节点都加入队列 + for (Node* child : cur->children) { + q.push(child); + } + } + // 当前层遍历完毕 + res.push_back(level); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func levelOrder(root *Node) [][]int { + res := [][]int{} + if root == nil { + return res + } + + // 以下是标准的 BFS 算法遍历框架 + q := []*Node{root} + + for len(q) != 0 { + sz := len(q) + level := []int{} + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + level = append(level, cur.Val) + // 多叉树可能有多个子节点,将所有子节点都加入队列 + for _, child := range cur.Children { + q = append(q, child) + } + } + // 当前层遍历完毕 + res = append(res, level) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> levelOrder(Node root) { + List> res = new LinkedList<>(); + if (root == null) { + return res; + } + + // 以下是标准的 BFS 算法遍历框架 + Queue q = new LinkedList<>(); + q.offer(root); + + while (!q.isEmpty()) { + int sz = q.size(); + List level = new LinkedList<>(); + for (int i = 0; i < sz; i++) { + Node cur = q.poll(); + level.add(cur.val); + // 多叉树可能有多个子节点,将所有子节点都加入队列 + for (Node child : cur.children) { + q.offer(child); + } + } + // 当前层遍历完毕 + res.add(level); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +// 将 Java 代码转化为 JavaScript 代码 +var levelOrder = function(root) { + // 创建一个数组 res 来存储遍历结果 + const res = [] + if (!root) { + // 如果结点为 null 则返回 res 数组 + return res + } + + // 创建一个队列 q 用于 BFS 遍历 + let q = [] + q.push(root) + + while (q.length > 0) { + // 记录当前队列中元素个数 + const sz = q.length + // 创建一个数组 level 来存储当前层所有结点的值 + const level = [] + for (let i = 0; i < sz; i++) { + // 弹出队头结点 cur 并将其值加入 level 数组中 + const cur = q.shift() + level.push(cur.val) + + // 将当前结点的所有子节点加入队列 q 中 + for (const child of cur.children) { + q.push(child) + } + } + + // 将当前层的值 level 加入到 res 数组中 + res.push(level) + } + + // 返回遍历结果 + return res +} +``` + +```python +# by chatGPT (python) +class Solution: + def levelOrder(self, root: 'Node') -> List[List[int]]: + res = [] + if not root: + return res + + # 以下是标准的 BFS 算法遍历框架 + q = deque() + q.append(root) + + while q: + sz = len(q) + level = [] + for i in range(sz): + cur = q.popleft() + level.append(cur.val) + # 多叉树可能有多个子节点,将所有子节点都加入队列 + for child in cur.children: + q.append(child) + # 当前层遍历完毕 + res.append(level) + return res +``` + +https://leetcode.cn/problems/n-ary-tree-level-order-traversal 的多语言解法👆 + +https://leetcode.cn/problems/n-ary-tree-postorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector postorder(Node* root) { + traverse(root); + return res; + } + +private: + vector res; + + void traverse(Node* root) { + if (root == nullptr) { + return; + } + // 前序遍历位置 + for (Node* child : root->children) { + traverse(child); + } + // 后序遍历位置 + res.push_back(root->val); + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a Node. + * type Node struct { + * Val int + * Children []*Node + * } + */ + +func postorder(root *Node) []int { + res := make([]int, 0) + traverse(root, &res) // use closure to avoid using global variable + return res +} + +func traverse(root *Node, res *[]int) { + if root == nil { + return + } + // 前序遍历位置 + for _, child := range root.Children { + traverse(child, res) + } + // 后序遍历位置 + *res = append(*res, root.Val) +} +``` + +```java +// by labuladong (java) +class Solution { + public List postorder(Node root) { + traverse(root); + return res; + } + + List res = new LinkedList<>(); + + void traverse(Node root) { + if (root == null) { + return; + } + // 前序遍历位置 + for (Node child : root.children) { + traverse(child); + } + // 后序遍历位置 + res.add(root.val); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {Node} root + * @return {number[]} + */ +var postorder = function(root) { + var res = []; + + var traverse = function(root) { + if (root === null) { + return; + } + // 前序遍历位置 + for (var i = 0; i < root.children.length; i++) { + traverse(root.children[i]); + } + // 后序遍历位置 + res.push(root.val); + }; + + traverse(root); + return res; +} +``` + +```python +# by chatGPT (python) +# Definition for a Node. +# class Node: +# def __init__(self, val=None, children=None): +# self.val = val +# self.children = children if children is not None else [] + +class Solution: + def postorder(self, root: 'Node') -> List[int]: + self.traverse(root) + return self.res + + res = [] + + def traverse(self, root: 'Node') -> None: + if not root: + return + # 前序遍历位置 + for child in root.children: + self.traverse(child) + # 后序遍历位置 + self.res.append(root.val) +``` + +https://leetcode.cn/problems/n-ary-tree-postorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/n-ary-tree-preorder-traversal 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector preorder(Node* root) { + traverse(root); + return res; + } + +private: + vector res; + + void traverse(Node* root) { + if (!root) { + return; + } + // 前序遍历位置 + res.push_back(root->val); + for (Node* child : root->children) { + traverse(child); + } + // 后序遍历位置 + } +}; +``` + +```go +// by chatGPT (go) +import "container/list" + +// Node Definition +type Node struct { + Val int + Children []*Node +} + +func preorder(root *Node) []int { + var res []int + traverse(root, &res) + return res +} + +func traverse(root *Node, res *[]int) { + if root == nil { + return + } + // 前序遍历位置 + *res = append(*res, root.Val) + for _, child := range root.Children { + traverse(child, res) + } + // 后序遍历位置 +} +``` + +```java +// by labuladong (java) +class Solution { + public List preorder(Node root) { + traverse(root); + return res; + } + + List res = new LinkedList<>(); + + void traverse(Node root) { + if (root == null) { + return; + } + // 前序遍历位置 + res.add(root.val); + for (Node child : root.children) { + traverse(child); + } + // 后序遍历位置 + } +} +``` + +```javascript +// by chatGPT (javascript) +var preorder = function(root) { + var res = []; + + var traverse = function(root) { + if (root === null) { + return; + } + // 前序遍历位置 + res.push(root.val); + for (var i = 0; i < root.children.length; i++) { + traverse(root.children[i]); + } + // 后序遍历位置 + } + + traverse(root); + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def preorder(self, root: 'Node') -> List[int]: + self.traverse(root) + return self.res + + def __init__(self): + self.res = [] + + def traverse(self, root: 'Node'): + if not root: + return + # 前序遍历位置 + self.res.append(root.val) + for child in root.children: + self.traverse(child) + # 后序遍历位置 +``` + +https://leetcode.cn/problems/n-ary-tree-preorder-traversal 的多语言解法👆 + +https://leetcode.cn/problems/n-queens 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector> res; + + /* 输入棋盘边长 n,返回所有合法的放置 */ + vector> solveNQueens(int n) { + // '.' 表示空,'Q' 表示皇后,初始化空棋盘。 + vector board(n, string(n, '.')); + backtrack(board, 0); + return res; + } + + // 路径:board 中小于 row 的那些行都已经成功放置了皇后 + // 选择列表:第 row 行的所有列都是放置皇后的选择 + // 结束条件:row 超过 board 的最后一行 + void backtrack(vector& board, int row) { + // 触发结束条件 + if (row == board.size()) { + res.push_back(board); + return; + } + + int n = board[row].size(); + for (int col = 0; col < n; col++) { + // 排除不合法选择 + if (!isValid(board, row, col)) { + /** + ![](../pictures/backtracking/7.jpg) + */ + continue; + } + // 做选择 + board[row][col] = 'Q'; + // 进入下一行决策 + backtrack(board, row + 1); + // 撤销选择 + board[row][col] = '.'; + } + } + + /* 是否可以在 board[row][col] 放置皇后?*/ + bool isValid(vector& board, int row, int col) { + int n = board.size(); + // 检查列是否有皇后互相冲突 + for (int i = 0; i <= row; i++) { + if (board[i][col] == 'Q') + return false; + } + // 检查右上方是否有皇后互相冲突 + for (int i = row - 1, j = col + 1; + i >= 0 && j < n; i--, j++) { + if (board[i][j] == 'Q') + return false; + } + // 检查左上方是否有皇后互相冲突 + for (int i = row - 1, j = col - 1; + i >= 0 && j >= 0; i--, j--) { + if (board[i][j] == 'Q') + return false; + } + return true; + } +}; +``` + +```go +// by chatGPT (go) +type backtracking struct { + res [][]string +} + +func (b *backtracking) solveNQueens(n int) [][]string { + board := make([]string, n) + for i := 0; i < n; i++ { + board[i] = strings.Repeat(".", n) + } + b.backtrack(board, 0) + return b.res +} + +func (b *backtracking) backtrack(board []string, row int) { + if row == len(board) { + temp := make([]string, len(board)) + copy(temp, board) + b.res = append(b.res, temp) + return + } + + n := len(board[row]) + for col := 0; col < n; col++ { + if !b.isValid(board, row, col) { + // 排除不合法选择 + continue + } + // 做选择 + board[row] = board[row][:col] + "Q" + board[row][col+1:] + // 进入下一行决策 + b.backtrack(board, row+1) + // 撤销选择 + board[row] = board[row][:col] + "." + board[row][col+1:] + } +} + +func (b *backtracking) isValid(board []string, row, col int) bool { + n := len(board) + // 检查列是否有皇后互相冲突 + for i := 0; i <= row; i++ { + if board[i][col] == 'Q' { + return false + } + } + // 检查右上方是否有皇后互相冲突 + for i, j := row-1, col+1; i >= 0 && j < n; i, j = i-1, j+1 { + if board[i][j] == 'Q' { + return false + } + } + // 检查左上方是否有皇后互相冲突 + for i, j := row-1, col-1; i >= 0 && j >= 0; i, j = i-1, j-1 { + if board[i][j] == 'Q' { + return false + } + } + return true +} +``` + +```java +// by chatGPT (java) +class Solution { + List> res = new ArrayList<>(); + + /** + * 输入棋盘边长 n,返回所有合法的放置 + */ + public List> solveNQueens(int n) { + // '.' 表示空,'Q' 表示皇后,初始化空棋盘。 + List board = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append('.'); + } + for (int i = 0; i < n; i++) { + board.add(sb.toString()); + } + backtrack(board, 0); + return res; + } + + /** + * *路径:board 中小于 row 的那些行都已经成功放置了皇后 + * *选择列表:第 row 行的所有列都是放置皇后的选择 + * *结束条件:row 超过 board 的最后一行 + */ + private void backtrack(List board, int row) { + if (row == board.size()) { + res.add(new ArrayList<>(board)); + return; + } + + int n = board.get(row).length(); + for (int col = 0; col < n; col++) { + // 排除不合法选择 + if (!isValid(board, row, col)) { + /** + ![](../pictures/backtracking/7.jpg) + */ + continue; + } + // 做选择 + char[] arr = board.get(row).toCharArray(); + arr[col] = 'Q'; + board.set(row, String.valueOf(arr)); + // 进入下一行决策 + backtrack(board, row + 1); + // 撤销选择 + arr[col] = '.'; + board.set(row, String.valueOf(arr)); + } + } + + /* 是否可以在 board[row][col] 放置皇后?*/ + private boolean isValid(List board, int row, int col) { + int n = board.size(); + + // 检查列是否有皇后互相冲突 + for (int i = 0; i <= row; i++) { + if (board.get(i).charAt(col) == 'Q') { + return false; + } + } + + // 检查右上方是否有皇后互相冲突 + for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { + if (board.get(i).charAt(j) == 'Q') { + return false; + } + } + + // 检查左上方是否有皇后互相冲突 + for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { + if (board.get(i).charAt(j) == 'Q') { + return false; + } + } + + return true; + } +} +``` + +```javascript +// by chatGPT (javascript) +var solveNQueens = function(n) { + var res = []; + + // '.' 表示空,'Q' 表示皇后,初始化空棋盘。 + var board = new Array(n); + for (var i = 0; i < n; i++) { + board[i] = new Array(n).fill('.'); + } + + backtrack(board, 0); + + return res; + + // 路径:board 中小于 row 的那些行都已经成功放置了皇后 + // 选择列表:第 row 行的所有列都是放置皇后的选择 + // 结束条件:row 超过 board 的最后一行 + function backtrack(board, row) { + // 触发结束条件 + if (row === board.length) { + res.push(Array.from(board, row => row.join(''))); + return; + } + + var n = board.length; + for (var col = 0; col < n; col++) { + // 排除不合法选择 + if (!isValid(board, row, col)) { + continue; + } + // 做选择 + board[row][col] = 'Q'; + // 进入下一行决策 + backtrack(board, row + 1); + // 撤销选择 + board[row][col] = '.'; + } + } + + /* 是否可以在 board[row][col] 放置皇后?*/ + function isValid(board, row, col) { + var n = board.length; + // 检查列是否有皇后互相冲突 + for (var i = 0; i <= row; i++) { + if (board[i][col] === 'Q') { + return false; + } + } + // 检查右上方是否有皇后互相冲突 + for (var i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { + if (board[i][j] === 'Q') { + return false; + } + } + // 检查左上方是否有皇后互相冲突 + for (var i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { + if (board[i][j] === 'Q') { + return false; + } + } + return true; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + + def solveNQueens(self, n: int) -> List[List[str]]: + board = [['.' for j in range(n)] for i in range(n)] # 初始化空棋盘 + self.backtrack(board, 0) + return self.res + + def backtrack(self, board: List[List[str]], row: int): + if row == len(board): # 触发结束条件 + self.res.append([''.join(board[i]) for i in range(len(board))]) + return + + for col in range(len(board[row])): + if not self.isValid(board, row, col): # 排除不合法选择 + continue + board[row][col] = 'Q' # 做选择 + self.backtrack(board, row + 1) # 进入下一行决策 + board[row][col] = '.' # 撤销选择 + + def isValid(self, board: List[List[str]], row: int, col: int) -> bool: + n = len(board) + # 检查列是否有皇后互相冲突 + for i in range(row + 1): + if board[i][col] == 'Q': + return False + # 检查右上方是否有皇后互相冲突 + for i, j in zip(range(row - 1, -1, -1), range(col + 1, n)): + if board[i][j] == 'Q': + return False + # 检查左上方是否有皇后互相冲突 + for i, j in zip(range(row - 1, -1, -1), range(col - 1, -1, -1)): + if board[i][j] == 'Q': + return False + return True +``` + +https://leetcode.cn/problems/n-queens 的多语言解法👆 + +https://leetcode.cn/problems/nZZqjQ 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int minEatingSpeed(vector& piles, int H) { + int left = 1; + int right = 1000000000 + 1; + while (left < right) { + int mid = left + (right - left) / 2; + if (f(piles, mid) <= H) { + right = mid; + } else { + left = mid + 1; + } + } + return left; + } + + // 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 + // f(x) 随着 x 的增加单调递减 + int f(vector& piles, int x) { + int hours = 0; + for (int pile : piles) { + hours += (pile + x - 1) / x; + } + return hours; + } +}; +``` + +```go +// by chatGPT (go) +// Go Solution Code for LeetCode Problem 875: Koko Eating Bananas + +func minEatingSpeed(piles []int, H int) int { + left := 1 + right := 1000000000 + 1 + + for left < right { + mid := left + (right - left) / 2 + if f(piles, mid) <= H { + right = mid + } else { + left = mid + 1 + } + } + return left +} + +// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 +// f(x) 随着 x 的增加单调递减 +func f(piles []int, x int) int { + hours := 0 + for i := 0; i < len(piles); i++ { + hours += piles[i] / x + if piles[i] % x > 0 { + hours++ + } + } + return hours +} +``` + +```java +// by labuladong (java) +class Solution { + public int minEatingSpeed(int[] piles, int H) { + int left = 1; + int right = 1000000000 + 1; + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(piles, mid) <= H) { + right = mid; + } else { + left = mid + 1; + } + } + return left; + } + + // 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 + // f(x) 随着 x 的增加单调递减 + int f(int[] piles, int x) { + int hours = 0; + for (int i = 0; i < piles.length; i++) { + hours += piles[i] / x; + if (piles[i] % x > 0) { + hours++; + } + } + return hours; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} piles + * @param {number} H + * @return {number} + */ +var minEatingSpeed = function(piles, H) { + let left = 1; + let right = 1000000000 + 1; + + while (left < right) { + let mid = left + Math.floor((right - left) / 2); + if (f(piles, mid) <= H) { + right = mid; + } else { + left = mid + 1; + } + } + return left; +}; + +// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 +// f(x) 随着 x 的增加单调递减 +function f(piles, x) { + let hours = 0; + for (let i = 0; i < piles.length; i++) { + hours += Math.floor(piles[i] / x); + if (piles[i] % x > 0) { + hours++; + } + } + return hours; +} +``` + +```python +# by chatGPT (python) +class Solution: + def minEatingSpeed(self, piles: List[int], H: int) -> int: + left = 1 + right = 1000000000 + 1 + + while left < right: + mid = left + (right - left) // 2 + if self.f(piles, mid) <= H: + right = mid + else: + left = mid + 1 + return left + + # 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 + # f(x) 随着 x 的增加单调递减 + def f(self, piles: List[int], x: int) -> int: + hours = 0 + for i in range(len(piles)): + hours += piles[i] // x + if piles[i] % x > 0: + hours += 1 + return hours +``` + +https://leetcode.cn/problems/nZZqjQ 的多语言解法👆 + +https://leetcode.cn/problems/nearest-exit-from-entrance-in-maze 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int nearestExit(vector>& maze, vector& entrance) { + int m = maze.size(); + int n = maze[0].size(); + vector> dirs {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + // BFS 算法的队列和 visited 数组 + queue> queue; + vector> visited(m, vector(n, false)); + queue.push(entrance); + visited[entrance[0]][entrance[1]] = true; + // 启动 BFS 算法从 entrance 开始像四周扩散 + int step = 0; + while (!queue.empty()) { + int sz = queue.size(); + step++; + // 扩散当前队列中的所有节点 + for (int i = 0; i < sz; i++) { + vector cur = queue.front(); + queue.pop(); + // 每个节点都会尝试向上下左右四个方向扩展一步 + for (vector dir : dirs) { + int x = cur[0] + dir[0]; + int y = cur[1] + dir[1]; + if (x < 0 || x >= m || y < 0 || y >= n + || visited[x][y] || maze[x][y] == '+') { + continue; + } + if (x == 0 || x == m - 1 || y == 0 || y == n - 1) { + // 走到边界(出口) + return step; + } + visited[x][y] = true; + queue.push(vector{x, y}); + } + } + } + return -1; + } +}; +``` + +```go +// by chatGPT (go) +func nearestExit(maze [][]byte, entrance []int) int { + m, n := len(maze), len(maze[0]) + dirs := [][]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}} + + // BFS 算法的队列和 visited 数组 + queue := [][]int{entrance} + visited := make([][]bool, m) + for i := 0; i < m; i++ { + visited[i] = make([]bool, n) + } + visited[entrance[0]][entrance[1]] = true + // 启动 BFS 算法从 entrance 开始像四周扩散 + step := 0 + for len(queue) > 0 { + sz := len(queue) + step++ + // 扩散当前队列中的所有节点 + for i := 0; i < sz; i++ { + cur := queue[0] + queue = queue[1:] + // 每个节点都会尝试向上下左右四个方向扩展一步 + for _, dir := range dirs { + x := cur[0] + dir[0] + y := cur[1] + dir[1] + if x < 0 || x >= m || y < 0 || y >= n || visited[x][y] || maze[x][y] == '+' { + continue + } + if x == 0 || x == m-1 || y == 0 || y == n-1 { + // 走到边界(出口) + return step + } + visited[x][y] = true + queue = append(queue, []int{x, y}) + } + } + } + return -1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int nearestExit(char[][] maze, int[] entrance) { + int m = maze.length; + int n = maze[0].length; + int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + // BFS 算法的队列和 visited 数组 + Queue queue = new LinkedList<>(); + boolean[][] visited = new boolean[m][n]; + queue.offer(entrance); + visited[entrance[0]][entrance[1]] = true; + // 启动 BFS 算法从 entrance 开始像四周扩散 + int step = 0; + while (!queue.isEmpty()) { + int sz = queue.size(); + step++; + // 扩散当前队列中的所有节点 + for (int i = 0; i < sz; i++) { + int[] cur = queue.poll(); + // 每个节点都会尝试向上下左右四个方向扩展一步 + for (int[] dir : dirs) { + int x = cur[0] + dir[0]; + int y = cur[1] + dir[1]; + if (x < 0 || x >= m || y < 0 || y >= n + || visited[x][y] || maze[x][y] == '+') { + continue; + } + if (x == 0 || x == m - 1 || y == 0 || y == n - 1) { + // 走到边界(出口) + return step; + } + visited[x][y] = true; + queue.offer(new int[]{x, y}); + } + } + } + return -1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var nearestExit = function(maze, entrance) { + const m = maze.length; + const n = maze[0].length; + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + + // BFS 算法的队列和 visited 数组 + const queue = []; + const visited = Array(m).fill(0).map(() => Array(n).fill(false)); + queue.push(entrance); + visited[entrance[0]][entrance[1]] = true; + + // 启动 BFS 算法从 entrance 开始像四周扩散 + let step = 0; + while (queue.length) { + const sz = queue.length; + step++; + + // 扩散当前队列中的所有节点 + for (let i = 0; i < sz; i++) { + const cur = queue.shift(); + + // 每个节点都会尝试向上下左右四个方向扩展一步 + for (const dir of dirs) { + const x = cur[0] + dir[0]; + const y = cur[1] + dir[1]; + + if (x < 0 || x >= m || y < 0 || y >= n || visited[x][y] || maze[x][y] === '+') { + continue; + } + + if (x === 0 || x === m - 1 || y === 0 || y === n - 1) { + // 走到边界(出口) + return step; + } + + visited[x][y] = true; + queue.push([x, y]); + } + } + } + + return -1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def nearestExit(self, maze: List[List[str]], entrance: List[int]) -> int: + m, n = len(maze), len(maze[0]) + dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)] + + # BFS 算法的队列和 visited 数组 + queue = deque() + visited = [[False] * n for _ in range(m)] + queue.append(tuple(entrance)) + visited[entrance[0]][entrance[1]] = True + # 启动 BFS 算法从 entrance 开始像四周扩散 + step = 0 + while queue: + sz = len(queue) + step += 1 + # 扩散当前队列中的所有节点 + for _ in range(sz): + cur_x, cur_y = queue.popleft() + # 每个节点都会尝试向上下左右四个方向扩展一步 + for dir_x, dir_y in dirs: + nxt_x, nxt_y = cur_x + dir_x, cur_y + dir_y + if nxt_x < 0 or nxt_x >= m or nxt_y < 0 or nxt_y >= n \ + or visited[nxt_x][nxt_y] or maze[nxt_x][nxt_y] == '+': + continue + if nxt_x == 0 or nxt_x == m - 1 or nxt_y == 0 or nxt_y == n - 1: + # 走到边界(出口) + return step + visited[nxt_x][nxt_y] = True + queue.append((nxt_x, nxt_y)) + return -1 +``` + +https://leetcode.cn/problems/nearest-exit-from-entrance-in-maze 的多语言解法👆 + +https://leetcode.cn/problems/network-delay-time 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int networkDelayTime(vector>& times, int n, int k) { + // 节点编号是从 1 开始的,所以要一个大小为 n + 1 的邻接表 + vector>> graph(n + 1); + for (int i = 1; i <= n; i++) { + graph[i] = vector>(); + } + // 构造图 + for (auto& edge : times) { + int from = edge[0]; + int to = edge[1]; + int weight = edge[2]; + // from -> List<(to, weight)> + // 邻接表存储图结构,同时存储权重信息 + graph[from].emplace_back(to, weight); + } + // 启动 dijkstra 算法计算以节点 k 为起点到其他节点的最短路径 + vector distTo = dijkstra(k, graph, n); + + // 找到最长的那一条最短路径 + int res = 0; + for (int i = 1; i < distTo.size(); i++) { + if (distTo[i] == INT_MAX) { + // 有节点不可达,返回 -1 + return -1; + } + res = max(res, distTo[i]); + } + return res; + } + +private: + // 输入一个起点 start,计算从 start 到其他节点的最短距离 + vector dijkstra(int start, vector>>& graph, int n) { + // 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重 + vector distTo(n + 1, INT_MAX); + // base case,start 到 start 的最短距离就是 0 + distTo[start] = 0; + + // 优先级队列,distFromStart 较小的排在前面 + priority_queue, vector>, greater>> pq; + // 从起点 start 开始进行 BFS + pq.emplace(0, start); + + while (!pq.empty()) { + auto [curDistFromStart, curNodeID] = pq.top(); + pq.pop(); + + if (curDistFromStart > distTo[curNodeID]) { + continue; + } + + // 将 curNode 的相邻节点装入队列 + for (auto& [nextNodeID, weight] : graph[curNodeID]) { + int distToNextNode = distTo[curNodeID] + weight; + // 更新 dp table + if (distTo[nextNodeID] > distToNextNode) { + distTo[nextNodeID] = distToNextNode; + pq.emplace(distToNextNode, nextNodeID); + } + } + } + return distTo; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "container/heap" + "math" +) + +func networkDelayTime(times [][]int, n int, k int) int { + // 节点编号是从 1 开始的,所以要一个大小为 n + 1 的邻接表 + graph := make([][][]int, n+1) + for i := 1; i <= n; i++ { + graph[i] = make([][]int, 0) + } + + // 构造图 + for _, edge := range times { + from := edge[0] + to := edge[1] + weight := edge[2] + // from -> [ [to, weight], ... ] + // 邻接表存储图结构,同时存储权重信息 + graph[from] = append(graph[from], []int{to, weight}) + } + + // 启动 dijkstra 算法计算以节点 k 为起点到其他节点的最短路径 + distTo := dijkstra(k, graph) + + // 找到最长的那一条最短路径 + res := 0 + for i := 1; i < len(distTo); i++ { + if distTo[i] == math.MaxInt32 { + // 有节点不可达,返回 -1 + return -1 + } + res = max(res, distTo[i]) + } + return res +} + +type State struct { + // 图节点的 id + id int + // 从 start 节点到当前节点的距离 + distFromStart int +} + +type PriorityQueue []*State + +func (pq PriorityQueue) Len() int { return len(pq) } +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].distFromStart < pq[j].distFromStart +} +func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] } + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(*State) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + old[n-1] = nil // avoid memory leak + *pq = old[0 : n-1] + return item +} + +// 输入一个起点 start,计算从 start 到其他节点的最短距离 +func dijkstra(start int, graph [][][]int) []int { + // 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重 + distTo := make([]int, len(graph)) + for i := 1; i < len(graph); i++ { + distTo[i] = math.MaxInt32 + } + // base case,start 到 start 的最短距离就是 0 + distTo[start] = 0 + + // 优先级队列,distFromStart 较小的排在前面 + pq := make(PriorityQueue, 0) + heap.Init(&pq) + // 从起点 start 开始进行 BFS + heap.Push(&pq, &State{id: start, distFromStart: 0}) + + for pq.Len() > 0 { + curState := heap.Pop(&pq).(*State) + curNodeID := curState.id + curDistFromStart := curState.distFromStart + + if curDistFromStart > distTo[curNodeID] { + continue + } + + // 将 curNode 的相邻节点装入队列 + for _, neighbor := range graph[curNodeID] { + nextNodeID := neighbor[0] + distToNextNode := distTo[curNodeID] + neighbor[1] + // 更新 dp table + if distTo[nextNodeID] > distToNextNode { + distTo[nextNodeID] = distToNextNode + heap.Push(&pq, &State{id: nextNodeID, distFromStart: distToNextNode}) + } + } + } + return distTo +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int networkDelayTime(int[][] times, int n, int k) { + // 节点编号是从 1 开始的,所以要一个大小为 n + 1 的邻接表 + List[] graph = new LinkedList[n + 1]; + for (int i = 1; i <= n; i++) { + graph[i] = new LinkedList<>(); + } + // 构造图 + for (int[] edge : times) { + int from = edge[0]; + int to = edge[1]; + int weight = edge[2]; + // from -> List<(to, weight)> + // 邻接表存储图结构,同时存储权重信息 + graph[from].add(new int[]{to, weight}); + } + // 启动 dijkstra 算法计算以节点 k 为起点到其他节点的最短路径 + int[] distTo = dijkstra(k, graph); + + // 找到最长的那一条最短路径 + int res = 0; + for (int i = 1; i < distTo.length; i++) { + if (distTo[i] == Integer.MAX_VALUE) { + // 有节点不可达,返回 -1 + return -1; + } + res = Math.max(res, distTo[i]); + } + return res; + } + + class State { + // 图节点的 id + int id; + // 从 start 节点到当前节点的距离 + int distFromStart; + + State(int id, int distFromStart) { + this.id = id; + this.distFromStart = distFromStart; + } + } + + // 输入一个起点 start,计算从 start 到其他节点的最短距离 + int[] dijkstra(int start, List[] graph) { + // 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重 + int[] distTo = new int[graph.length]; + Arrays.fill(distTo, Integer.MAX_VALUE); + // base case,start 到 start 的最短距离就是 0 + distTo[start] = 0; + + // 优先级队列,distFromStart 较小的排在前面 + Queue pq = new PriorityQueue<>((a, b) -> { + return a.distFromStart - b.distFromStart; + }); + // 从起点 start 开始进行 BFS + pq.offer(new State(start, 0)); + + while (!pq.isEmpty()) { + State curState = pq.poll(); + int curNodeID = curState.id; + int curDistFromStart = curState.distFromStart; + + if (curDistFromStart > distTo[curNodeID]) { + continue; + } + + // 将 curNode 的相邻节点装入队列 + for (int[] neighbor : graph[curNodeID]) { + int nextNodeID = neighbor[0]; + int distToNextNode = distTo[curNodeID] + neighbor[1]; + // 更新 dp table + if (distTo[nextNodeID] > distToNextNode) { + distTo[nextNodeID] = distToNextNode; + pq.offer(new State(nextNodeID, distToNextNode)); + } + } + } + return distTo; + } +} +``` + +```javascript +// by chatGPT (javascript) +var networkDelayTime = function(times, n, k) { + // 节点编号是从 1 开始的,所以要一个大小为 n + 1 的邻接表 + let graph = new Array(n + 1); + for (let i = 1; i <= n; i++) { + graph[i] = []; + } + // 构造图 + for (let edge of times) { + let from = edge[0]; + let to = edge[1]; + let weight = edge[2]; + // from -> List<(to, weight)> + // 邻接表存储图结构,同时存储权重信息 + graph[from].push([to, weight]); + } + // 启动 dijkstra 算法计算以节点 k 为起点到其他节点的最短路径 + let distTo = dijkstra(k, graph); + + // 找到最长的那一条最短路径 + let res = 0; + for (let i = 1; i < distTo.length; i++) { + if (distTo[i] === Infinity) { + // 有节点不可达,返回 -1 + return -1; + } + res = Math.max(res, distTo[i]); + } + return res; +}; + +class State { + // 图节点的 id + constructor(id, distFromStart) { + this.id = id; + // 从 start 节点到当前节点的距离 + this.distFromStart = distFromStart; + } +} + +// 输入一个起点 start,计算从 start 到其他节点的最短距离 +function dijkstra(start, graph) { + // 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重 + let distTo = new Array(graph.length).fill(Infinity); + // base case,start 到 start 的最短距离就是 0 + distTo[start] = 0; + + // 优先级队列,distFromStart 较小的排在前面 + let pq = []; + // 从起点 start 开始进行 BFS + pq.push(new State(start, 0)); + pq.sort((a, b) => { + return a.distFromStart - b.distFromStart; + }); + + while (pq.length > 0) { + let curState = pq.shift(); + let curNodeID = curState.id; + let curDistFromStart = curState.distFromStart; + + if (curDistFromStart > distTo[curNodeID]) { + continue; + } + + // 将 curNode 的相邻节点装入队列 + for (let neighbor of graph[curNodeID]) { + let nextNodeID = neighbor[0]; + let distToNextNode = distTo[curNodeID] + neighbor[1]; + // 更新 dp table + if (distTo[nextNodeID] > distToNextNode) { + distTo[nextNodeID] = distToNextNode; + pq.push(new State(nextNodeID, distToNextNode)); + pq.sort((a, b) => { + return a.distFromStart - b.distFromStart; + }); + } + } + } + return distTo; +} +``` + +```python +# by chatGPT (python) +import heapq +from typing import List + +class Solution: + def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int: + # 节点编号是从 1 开始的,所以要一个大小为 n + 1 的邻接表 + graph = [[] for _ in range(n+1)] + for edge in times: + from_node, to_node, weight = edge[0], edge[1], edge[2] + # from -> List<(to, weight)> + # 邻接表存储图结构,同时存储权重信息 + graph[from_node].append((to_node, weight)) + # 启动 dijkstra 算法计算以节点 k 为起点到其他节点的最短路径 + dist_to = self.dijkstra(k, graph) + + # 找到最长的那一条最短路径 + res = 0 + for i in range(1, len(dist_to)): + if dist_to[i] == float('inf'): + # 有节点不可达,返回 -1 + return -1 + res = max(res, dist_to[i]) + return res + + class State: + # 图节点的 id + def __init__(self, id: int, dist_from_start: int): + self.id = id + # 从 start 节点到当前节点的距离 + self.dist_from_start = dist_from_start + + def __lt__(self, other): + return self.dist_from_start < other.dist_from_start + + # 输入一个起点 start,计算从 start 到其他节点的最短距离 + def dijkstra(self, start: int, graph: List[List[int]]) -> List[int]: + # 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重 + dist_to = [float('inf')] * len(graph) + # base case,start 到 start 的最短距离就是 0 + dist_to[start] = 0 + + # 优先级队列,distFromStart 较小的排在前面 + pq = [Solution.State(start, 0)] + # 从起点 start 开始进行 BFS + heapq.heapify(pq) + + while pq: + cur_state = heapq.heappop(pq) + cur_node_id = cur_state.id + cur_dist_from_start = cur_state.dist_from_start + + if cur_dist_from_start > dist_to[cur_node_id]: + continue + + # 将 cur_node 的相邻节点装入队列 + for neighbor in graph[cur_node_id]: + next_node_id, dist_to_next_node = neighbor[0], dist_to[cur_node_id] + neighbor[1] + # 更新 dp table + if dist_to[next_node_id] > dist_to_next_node: + dist_to[next_node_id] = dist_to_next_node + heapq.heappush(pq, Solution.State(next_node_id, dist_to_next_node)) + return dist_to +``` + +https://leetcode.cn/problems/network-delay-time 的多语言解法👆 + +https://leetcode.cn/problems/next-greater-element-i 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector nextGreaterElement(vector& nums1, vector& nums2) { + // 记录 nums2 中每个元素的下一个更大元素 + vector greater = nextGreaterElement(nums2); + // 转化成映射:元素 x -> x 的下一个最大元素 + map greaterMap; + for (int i = 0; i < nums2.size(); i++) { + greaterMap[nums2[i]] = greater[i]; + } + // nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果 + vector res(nums1.size()); + for (int i = 0; i < nums1.size(); i++) { + res[i] = greaterMap[nums1[i]]; + } + return res; + } + + // 计算 nums 中每个元素的下一个更大元素 + vector nextGreaterElement(vector& nums) { + int n = nums.size(); + // 存放答案的数组 + vector res(n); + stack s; + // 倒着往栈里放 + for (int i = n - 1; i >= 0; i--) { + // 判定个子高矮 + while (!s.empty() && s.top() <= nums[i]) { + // 矮个起开,反正也被挡着了。。。 + s.pop(); + } + // nums[i] 身后的下一个更大元素 + res[i] = s.empty() ? -1 : s.top(); + s.push(nums[i]); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +import "fmt" + +func nextGreaterElement(nums1 []int, nums2 []int) []int { + // 记录 nums2 中每个元素的下一个更大元素 + greater := nextGreater(nums2) + // 转化成映射:元素 x -> x 的下一个最大元素 + greaterMap := make(map[int]int) + for i, v := range nums2 { + greaterMap[v] = greater[i] + } + // nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果 + res := make([]int, len(nums1)) + for i, v := range nums1 { + res[i] = greaterMap[v] + } + return res +} + +// 计算 nums 中每个元素的下一个更大元素 +func nextGreater(nums []int) []int { + n := len(nums) + // 存放答案的数组 + res := make([]int, n) + s := []int{} + // 倒着往栈里放 + for i := n - 1; i >= 0; i-- { + // 判定个子高矮 + for len(s) != 0 && s[len(s)-1] <= nums[i] { + // 矮个起开,反正也被挡着了。。。 + s = s[:len(s)-1] + } + // nums[i] 身后的下一个更大元素 + if len(s) == 0 { + res[i] = -1 + } else { + res[i] = s[len(s)-1] + } + s = append(s, nums[i]) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] nextGreaterElement(int[] nums1, int[] nums2) { + // 记录 nums2 中每个元素的下一个更大元素 + int[] greater = nextGreaterElement(nums2); + // 转化成映射:元素 x -> x 的下一个最大元素 + HashMap greaterMap = new HashMap<>(); + for (int i = 0; i < nums2.length; i++) { + greaterMap.put(nums2[i], greater[i]); + } + // nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果 + int[] res = new int[nums1.length]; + for (int i = 0; i < nums1.length; i++) { + res[i] = greaterMap.get(nums1[i]); + } + return res; + } + + // 计算 nums 中每个元素的下一个更大元素 + int[] nextGreaterElement(int[] nums) { + int n = nums.length; + // 存放答案的数组 + int[] res = new int[n]; + Stack s = new Stack<>(); + // 倒着往栈里放 + for (int i = n - 1; i >= 0; i--) { + // 判定个子高矮 + while (!s.isEmpty() && s.peek() <= nums[i]) { + // 矮个起开,反正也被挡着了。。。 + s.pop(); + } + // nums[i] 身后的下一个更大元素 + res[i] = s.isEmpty() ? -1 : s.peek(); + s.push(nums[i]); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var nextGreaterElement = function(nums1, nums2) { + // 记录 nums2 中每个元素的下一个更大元素 + var greater = nextGreater(nums2); + // 转化成映射:元素 x -> x 的下一个最大元素 + var greaterMap = new Map(); + for (var i = 0; i < nums2.length; i++) { + greaterMap.set(nums2[i], greater[i]); + } + // nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果 + var res = new Array(nums1.length); + for (var i = 0; i < nums1.length; i++) { + res[i] = greaterMap.get(nums1[i]); + } + return res; +}; + +// 计算 nums 中每个元素的下一个更大元素 +var nextGreater = function(nums) { + var n = nums.length; + // 存放答案的数组 + var res = new Array(n); + var s = []; + // 倒着往栈里放 + for (var i = n - 1; i >= 0; i--) { + // 判定个子高矮 + while (s.length && s[s.length-1] <= nums[i]) { + // 矮个起开,反正也被挡着了。。。 + s.pop(); + } + // nums[i] 身后的下一个更大元素 + res[i] = s.length == 0 ? -1 : s[s.length-1]; + s.push(nums[i]); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: + # 记录 nums2 中每个元素的下一个更大元素 + greater = self.nextGreater(nums2) + # 转化成映射:元素 x -> x 的下一个最大元素 + greaterMap = {} + for i in range(len(nums2)): + greaterMap[nums2[i]] = greater[i] + # nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果 + res = [] + for num in nums1: + res.append(greaterMap[num]) + return res + + # 计算 nums 中每个元素的下一个更大元素 + def nextGreater(self, nums: List[int]) -> List[int]: + n = len(nums) + # 存放答案的数组 + res = [-1] * n + s = [] + # 倒着往栈里放 + for i in range(n - 1, -1, -1): + # 判定个子高矮 + while s and s[-1] <= nums[i]: + # 矮个起开,反正也被挡着了。。。 + s.pop() + # nums[i] 身后的下一个更大元素 + if s: + res[i] = s[-1] + s.append(nums[i]) + return res +``` + +https://leetcode.cn/problems/next-greater-element-i 的多语言解法👆 + +https://leetcode.cn/problems/nim-game 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool canWinNim(int n) { + // 如果上来就踩到 4 的倍数,那就认输吧 + // 否则,可以把对方控制在 4 的倍数,必胜 + return n % 4 != 0; + } +}; +``` + +```go +// by chatGPT (go) +func canWinNim(n int) bool { + // 如果上来就踩到 4 的倍数,那就认输吧 + // 否则,可以把对方控制在 4 的倍数,必胜 + return n % 4 != 0 +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean canWinNim(int n) { + // 如果上来就踩到 4 的倍数,那就认输吧 + // 否则,可以把对方控制在 4 的倍数,必胜 + return n % 4 != 0; + } +} +``` + +```javascript +// by chatGPT (javascript) +var canWinNim = function(n) { + // 如果上来就踩到 4 的倍数,那就认输吧 + // 否则,可以把对方控制在 4 的倍数,必胜 + return n % 4 !== 0; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def canWinNim(self, n: int) -> bool: + # 如果上来就踩到 4 的倍数,那就认输吧 + # 否则,可以把对方控制在 4 的倍数,必胜 + return n % 4 != 0 +``` + +https://leetcode.cn/problems/nim-game 的多语言解法👆 + +https://leetcode.cn/problems/non-decreasing-subsequences 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> findSubsequences(vector& nums) { + if (nums.empty()) { + return res; + } + backtrack(nums, 0); + return res; + } + +private: + vector> res; + // 记录回溯的路径 + deque track; + + // 回溯算法主函数 + void backtrack(vector& nums, int start) { + if (track.size() >= 2) { + // 找到一个合法答案 + res.push_back(vector(track.begin(), track.end())); + } + // 用哈希集合防止重复选择相同元素 + unordered_set used; + // 回溯算法标准框架 + for (int i = start; i < nums.size(); i++) { + // 保证集合中元素都是递增顺序 + if (!track.empty() && track.back() > nums[i]) { + continue; + } + // 保证不要重复使用相同的元素 + if (used.count(nums[i])) { + continue; + } + // 选择 nums[i] + used.insert(nums[i]); + track.push_back(nums[i]); + // 递归遍历下一层回溯树 + backtrack(nums, i + 1); + // 撤销选择 nums[i] + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +func findSubsequences(nums []int) [][]int { + res := [][]int{} + if len(nums) == 0 { + return res + } + track := []int{} + backtrack(nums, 0, &track, &res) + return res +} + +// 回溯算法主函数 +func backtrack(nums []int, start int, track *[]int, res *[][]int) { + if len(*track) >= 2 { + // 找到一个合法答案 + temp := make([]int, len(*track)) + copy(temp, *track) + *res = append(*res, temp) + } + // 用哈希集合防止重复选择相同元素 + used := make(map[int]bool) + // 回溯算法标准框架 + for i := start; i < len(nums); i++ { + // 保证集合中元素都是递增顺序 + if len(*track) > 0 && (*track)[len(*track)-1] > nums[i] { + continue + } + // 保证不要重复使用相同的元素 + if used[nums[i]] { + continue + } + // 选择 nums[i] + used[nums[i]] = true + *track = append(*track, nums[i]) + // 递归遍历下一层回溯树 + backtrack(nums, i+1, track, res) + // 撤销选择 nums[i] + *track = (*track)[:len(*track)-1] + } +} +``` + +```java +// by labuladong (java) +class Solution { + public List> findSubsequences(int[] nums) { + if (nums.length == 0) { + return res; + } + backtrack(nums, 0); + return res; + } + + List> res = new LinkedList<>(); + // 记录回溯的路径 + LinkedList track = new LinkedList<>(); + + // 回溯算法主函数 + void backtrack(int[] nums, int start) { + if (track.size() >= 2) { + // 找到一个合法答案 + res.add(new LinkedList<>(track)); + } + // 用哈希集合防止重复选择相同元素 + HashSet used = new HashSet<>(); + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 保证集合中元素都是递增顺序 + if (!track.isEmpty() && track.getLast() > nums[i]) { + continue; + } + // 保证不要重复使用相同的元素 + if (used.contains(nums[i])) { + /** + ![](../pictures/排列组合/9.jpeg) + */ + continue; + } + // 选择 nums[i] + used.add(nums[i]); + track.add(nums[i]); + // 递归遍历下一层回溯树 + backtrack(nums, i + 1); + // 撤销选择 nums[i] + track.removeLast(); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number[][]} + */ +var findSubsequences = function(nums) { + var res = []; + // 记录回溯的路径 + var track = []; + + // 回溯算法主函数 + var backtrack = function(nums, start) { + if (track.length >= 2) { + // 找到一个合法答案 + res.push([...track]); + } + // 用哈希集合防止重复选择相同元素 + var used = new Set(); + // 回溯算法标准框架 + for (var i = start; i < nums.length; i++) { + // 保证集合中元素都是递增顺序 + if (track.length!==0 && track[track.length-1] > nums[i]) { + continue; + } + // 保证不要重复使用相同的元素 + if (used.has(nums[i])) { + /** + ![](../pictures/排列组合/9.jpeg) + */ + continue; + } + // 选择 nums[i] + used.add(nums[i]); + track.push(nums[i]); + // 递归遍历下一层回溯树 + backtrack(nums, i + 1); + // 撤销选择 nums[i] + track.pop(); + } + } + + if (nums.length === 0) { + return res; + } + backtrack(nums, 0); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findSubsequences(self, nums: List[int]) -> List[List[int]]: + if len(nums) == 0: + return self.res + self.backtrack(nums, 0) + return self.res + + # Initialize empty list for storing result + res = [] + # Initialize empty list for storing the path + track = [] + + # Backtracking algorithm main function + def backtrack(self, nums: List[int], start: int) -> None: + # If a valid answer is found (path has length 2 or greater), add to the result list + if len(self.track) >= 2: + self.res.append(self.track[:]) + # Use hash set to avoid selecting duplicate elements + used = set() + # Standard backtracking algorithm framework + for i in range(start, len(nums)): + # Ensure the elements in the track list are in increasing order + if len(self.track) > 0 and self.track[-1] > nums[i]: + continue + # Avoid using the same element multiple times + if nums[i] in used: + # Extend up -200 + # ![](../pictures/排列组合/9.jpeg) + continue + used.add(nums[i]) + self.track.append(nums[i]) + self.backtrack(nums, i+1) + self.track.pop() +``` + +https://leetcode.cn/problems/non-decreasing-subsequences 的多语言解法👆 + +https://leetcode.cn/problems/non-overlapping-intervals 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int eraseOverlapIntervals(vector>& intervals) { + int n = intervals.size(); + return n - intervalSchedule(intervals); + } + + // 区间调度算法,算出 intvs 中最多有几个互不相交的区间 + int intervalSchedule(vector>& intvs) { + if (intvs.empty()) return 0; + // 按 end 升序排序 + sort(intvs.begin(), intvs.end(), [](const vector& a, const vector& b){ + return a[1] < b[1]; + }); + // 至少有一个区间不相交 + int count = 1; + // 排序后,第一个区间就是 x + int x_end = intvs[0][1]; + for (const auto& interval : intvs) { + int start = interval[0]; + if (start >= x_end) { + // 找到下一个选择的区间了 + count++; + x_end = interval[1]; + } + } + return count; + } +}; +``` + +```go +// by chatGPT (go) +import "sort" + +func eraseOverlapIntervals(intervals [][]int) int { + n := len(intervals) + return n - intervalSchedule(intervals) +} + +// 区间调度算法,算出 intvs 中最多有几个互不相交的区间 +func intervalSchedule(intvs [][]int) int { + if len(intvs) == 0 { + return 0 + } + // 按 end 升序排序 + sort.Slice(intvs, func(i, j int) bool { + return intvs[i][1] < intvs[j][1] + }) + // 至少有一个区间不相交 + count := 1 + // 排序后,第一个区间就是 x + xEnd := intvs[0][1] + for _, interval := range intvs { + start := interval[0] + if start >= xEnd { + // 找到下一个选择的区间了 + count++ + xEnd = interval[1] + } + } + return count +} +``` + +```java +// by labuladong (java) +class Solution { + public int eraseOverlapIntervals(int[][] intervals) { + int n = intervals.length; + return n - intervalSchedule(intervals); + } + + // 区间调度算法,算出 intvs 中最多有几个互不相交的区间 + int intervalSchedule(int[][] intvs) { + if (intvs.length == 0) return 0; + // 按 end 升序排序 + Arrays.sort(intvs, new Comparator() { + public int compare(int[] a, int[] b) { + return a[1] - b[1]; + } + }); + // 至少有一个区间不相交 + int count = 1; + // 排序后,第一个区间就是 x + int x_end = intvs[0][1]; + for (int[] interval : intvs) { + int start = interval[0]; + if (start >= x_end) { + // 找到下一个选择的区间了 + count++; + x_end = interval[1]; + } + } + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} intervals + * @return {number} + */ +var eraseOverlapIntervals = function(intervals) { + var n = intervals.length; + return n - intervalSchedule(intervals); +}; + +/** + * 区间调度算法,算出 intvs 中最多有几个互不相交的区间 + * @param {number[][]} intvs + * @return {number} + */ +var intervalSchedule = function(intvs) { + if (intvs.length === 0) return 0; + // 按 end 升序排序 + intvs.sort((a, b) => a[1] - b[1]); + // 至少有一个区间不相交 + var count = 1; + // 排序后,第一个区间就是 x + var x_end = intvs[0][1]; + for (var i = 1; i < intvs.length; i++) { + var start = intvs[i][0]; + if (start >= x_end) { + // 找到下一个选择的区间了 + count++; + x_end = intvs[i][1]; + } + } + return count; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: + n = len(intervals) + return n - self.intervalSchedule(intervals) + + # 区间调度算法,算出 intvs 中最多有几个互不相交的区间 + def intervalSchedule(self, intvs: List[List[int]]) -> int: + if len(intvs) == 0: + return 0 + # 按 end 升序排序 + intvs = sorted(intvs, key=lambda x:x[1]) + # 至少有一个区间不相交 + count = 1 + # 排序后,第一个区间就是 x + x_end = intvs[0][1] + for interval in intvs: + start = interval[0] + if start >= x_end: + # 找到下一个选择的区间了 + count += 1 + x_end = interval[1] + return count +``` + +https://leetcode.cn/problems/non-overlapping-intervals 的多语言解法👆 + +https://leetcode.cn/problems/nth-digit 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findNthDigit(int n) { + // 位数(一位数,两位数...) + int digit = 1; + // 1,10,100, 1000 这样的后缀 + long long base = 1; + + while (n > 9 * base * digit) { + n -= 9 * base * digit; + base *= 10; + digit++; + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + long long val = base + (n - 1) / digit; + // 是这个三位数的第几位呢?这样算: + int index = (n - 1) % digit; + + // 怎么把 val 的第 index 这一位数字抠出来呢?可以转化成字符串来算: + return to_string(val)[index] - '0'; + } +}; +``` + +```go +// by chatGPT (go) +func findNthDigit(n int) int { + // 位数(一位数,两位数...) + digit := 1 + // 1,10,100, 1000 这样的后缀 + base := int64(1) + + for n > 9*base*int64(digit) { + n -= 9 * int(base) * digit + base *= 10 + digit++ + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + val := base + (int64(n)-1)/int64(digit) + // 是这个三位数的第几位呢?这样算: + index := (n - 1) % digit + + // 怎么把 val 的第 index 这一位数字抠出来呢?可以转化成字符串来算: + return int(strconv.Itoa(int(val))[index] - '0') +} +``` + +```java +// by labuladong (java) +class Solution { + public int findNthDigit(int n) { + // 位数(一位数,两位数...) + int digit = 1; + // 1,10,100, 1000 这样的后缀 + long base = 1; + + while (n > 9 * base * digit) { + n -= 9 * base * digit; + base *= 10; + digit++; + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + long val = base + (n - 1) / digit; + // 是这个三位数的第几位呢?这样算: + int index = (n - 1) % digit; + + // 怎么把 val 的第 index 这一位数字抠出来呢?可以转化成字符串来算: + return ("" + val).charAt(index) - '0'; + } +} +``` + +```javascript +// by chatGPT (javascript) +var findNthDigit = function(n) { + // 位数(一位数,两位数...) + let digit = 1; + // 1,10,100, 1000 这样的后缀 + let base = 1; + + while (n > 9 * base * digit) { + n -= 9 * base * digit; + base *= 10; + digit++; + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + let val = base + Math.floor((n - 1) / digit); + // 是这个三位数的第几位呢?这样算: + let index = (n - 1) % digit; + + // 怎么把 val 的第 index 这一位数字抠出来呢?可以转化成字符串来算: + return ("" + val).charAt(index) - '0'; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findNthDigit(self, n: int) -> int: + # 位数(一位数,两位数...) + digit = 1 + # 1,10,100, 1000 这样的后缀 + base = 1 + + while n > 9 * base * digit: + n -= 9 * base * digit + base *= 10 + digit += 1 + + # 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + # 哪个三位数呢?这样算: + val = base + (n - 1) // digit + # 是这个三位数的第几位呢?这样算: + index = (n - 1) % digit + + # 怎么把 val 的第 index 这一位数字抠出来呢?可以转化成字符串来算: + return int(str(val)[index]) +``` + +https://leetcode.cn/problems/nth-digit 的多语言解法👆 + +https://leetcode.cn/problems/number-of-1-bits 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // you need to treat n as an unsigned value + int hammingWeight(uint32_t n) { + int res = 0; + while (n != 0) { + n = n & (n - 1); + res++; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func hammingWeight(n uint32) int { + res := 0 + for n != 0 { + n &= n - 1 + res++ + } + return res +} +``` + +```java +// by labuladong (java) +public class Solution { + // you need to treat n as an unsigned value + public int hammingWeight(int n) { + int res = 0; + while (n != 0) { + n = n & (n - 1); + res++; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var hammingWeight = function(n) { + // you need to treat n as an unsigned value + var res = 0; + while (n !== 0) { + n = n & (n - 1); + res++; + } + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + # you need to treat n as an unsigned value + def hammingWeight(self, n: int) -> int: + res = 0 + while n != 0: + n = n & (n-1) + res += 1 + return res +``` + +https://leetcode.cn/problems/number-of-1-bits 的多语言解法👆 + +https://leetcode.cn/problems/number-of-closed-islands 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 主函数:计算封闭岛屿的数量 + int closedIsland(vector>& grid) { + int m = grid.size(), n = grid[0].size(); + for (int j = 0; j < n; j++) { + // 把靠上边的岛屿淹掉 + dfs(grid, 0, j); + // 把靠下边的岛屿淹掉 + dfs(grid, m - 1, j); + } + for (int i = 0; i < m; i++) { + // 把靠左边的岛屿淹掉 + dfs(grid, i, 0); + // 把靠右边的岛屿淹掉 + dfs(grid, i, n - 1); + } + // 遍历 grid,剩下的岛屿都是封闭岛屿 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) { + res++; + dfs(grid, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(vector>& grid, int i, int j) { + int m = grid.size(), n = grid[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 1) { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = 1; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +}; +``` + +```go +// by chatGPT (go) +// 计算封闭岛屿的数量 +func closedIsland(grid [][]int) int { + m, n := len(grid), len(grid[0]) + for j := 0; j < n; j++ { + // 把靠上边的岛屿淹掉 + dfs(grid, 0, j) + // 把靠下边的岛屿淹掉 + dfs(grid, m-1, j) + } + for i := 0; i < m; i++ { + // 把靠左边的岛屿淹掉 + dfs(grid, i, 0) + // 把靠右边的岛屿淹掉 + dfs(grid, i, n-1) + } + // 遍历 grid,剩下的岛屿都是封闭岛屿 + res := 0 + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid[i][j] == 0 { + res++ + dfs(grid, i, j) + } + } + } + return res +} + +// 从 (i, j) 开始,将与之相邻的陆地都变成海水 +func dfs(grid [][]int, i, j int) { + m, n := len(grid), len(grid[0]) + if i < 0 || j < 0 || i >= m || j >= n { + return + } + if grid[i][j] == 1 { + // 已经是海水了 + return + } + // 将 (i, j) 变成海水 + grid[i][j] = 1 + // 淹没上下左右的陆地 + dfs(grid, i+1, j) + dfs(grid, i, j+1) + dfs(grid, i-1, j) + dfs(grid, i, j-1) +} +``` + +```java +// by labuladong (java) +class Solution { + // 主函数:计算封闭岛屿的数量 + public int closedIsland(int[][] grid) { + int m = grid.length, n = grid[0].length; + for (int j = 0; j < n; j++) { + // 把靠上边的岛屿淹掉 + dfs(grid, 0, j); + // 把靠下边的岛屿淹掉 + dfs(grid, m - 1, j); + } + for (int i = 0; i < m; i++) { + // 把靠左边的岛屿淹掉 + dfs(grid, i, 0); + // 把靠右边的岛屿淹掉 + dfs(grid, i, n - 1); + } + // 遍历 grid,剩下的岛屿都是封闭岛屿 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) { + res++; + dfs(grid, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 1) { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = 1; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +} +``` + +```javascript +// by chatGPT (javascript) +var closedIsland = function(grid) { + var m = grid.length, n = grid[0].length; + for (var j = 0; j < n; j++) { + // 把靠上边的岛屿淹掉 + dfs(grid, 0, j); + // 把靠下边的岛屿淹掉 + dfs(grid, m - 1, j); + } + for (var i = 0; i < m; i++) { + // 把靠左边的岛屿淹掉 + dfs(grid, i, 0); + // 把靠右边的岛屿淹掉 + dfs(grid, i, n - 1); + } + // 遍历 grid,剩下的岛屿都是封闭岛屿 + var res = 0; + for (var i = 0; i < m; i++) { + for (var j = 0; j < n; j++) { + if (grid[i][j] === 0) { + res++; + dfs(grid, i, j); + } + } + } + return res; +} + +// 从 (i, j) 开始,将与之相邻的陆地都变成海水 +function dfs(grid, i, j) { + var m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] === 1) { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = 1; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); +} +``` + +```python +# by chatGPT (python) +class Solution: + def closedIsland(self, grid: List[List[int]]) -> int: + m, n = len(grid), len(grid[0]) + for j in range(n): + # 把靠上边的岛屿淹掉 + self.dfs(grid, 0, j) + # 把靠下边的岛屿淹掉 + self.dfs(grid, m - 1, j) + for i in range(m): + # 把靠左边的岛屿淹掉 + self.dfs(grid, i, 0) + # 把靠右边的岛屿淹掉 + self.dfs(grid, i, n - 1) + # 遍历 grid,剩下的岛屿都是封闭岛屿 + res = 0 + for i in range(m): + for j in range(n): + if grid[i][j] == 0: + res += 1 + self.dfs(grid, i, j) + return res + + # 从 (i, j) 开始,将与之相邻的陆地都变成海水 + def dfs(self, grid: List[List[int]], i: int, j: int) -> None: + m, n = len(grid), len(grid[0]) + if i < 0 or j < 0 or i >= m or j >= n: + return + if grid[i][j] == 1: + # 已经是海水了 + return + # 将 (i, j) 变成海水 + grid[i][j] = 1 + # 淹没上下左右的陆地 + self.dfs(grid, i + 1, j) + self.dfs(grid, i, j + 1) + self.dfs(grid, i - 1, j) + self.dfs(grid, i, j - 1) +``` + +https://leetcode.cn/problems/number-of-closed-islands 的多语言解法👆 + +https://leetcode.cn/problems/number-of-distinct-islands 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int numDistinctIslands(vector>& grid) { + int m = grid.size(), n = grid[0].size(); + // 记录所有岛屿的序列化结果 + unordered_set islands; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹掉这个岛屿,同时存储岛屿的序列化结果 + string s; + // 初始的方向可以随便写,不影响正确性 + dfs(grid, i, j, s, 's'); // 用字符代替数字,方便后续拼接 + islands.insert(s); + /** + ![](../pictures/岛屿/6.png) + */ + } + } + } + // 不相同的岛屿数量 + return islands.size(); + } + +private: + void dfs(vector>& grid, int i, int j, string& s, char dir) { + int m = grid.size(), n = grid[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n + || grid[i][j] == 0) { + return; + } + // 前序遍历位置:进入 (i, j) + grid[i][j] = 0; + s += dir; + dfs(grid, i - 1, j, s, 'u'); // 上 + dfs(grid, i + 1, j, s, 'd'); // 下 + dfs(grid, i, j - 1, s, 'l'); // 左 + dfs(grid, i, j + 1, s, 'r'); // 右 + + // 后序遍历位置:离开 (i, j) + s += 'b'; + } +}; +``` + +```go +// by chatGPT (go) +// LeetCode问题“不同岛屿的数量”的解决方案: https://leetcode.com/problems/number-of-distinct-islands/ +// 作者:Obadiah Crowe +// 时间:2021年7月29日 +import ( + "fmt" + "strconv" + "strings" +) + +func numDistinctIslands(grid [][]int) int { + // 计算行数和列数 + m, n := len(grid), len(grid[0]) + + // 存储所有不同岛屿的序列化结果 + islands := make(map[string]bool) + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid[i][j] == 1 { + // 淹没此岛屿,同时存储其序列化结果 + var sb strings.Builder + // 初始方向可以是任意的,不会影响正解 + dfs(grid, i, j, &sb, 666) + islands[sb.String()] = true + /**<向上扩展-200> + ![](../pictures/岛屿/6.png) + */ + } + } + } + + // 岛屿数量即为不同岛屿序列化结果的数量 + return len(islands) +} + +func dfs(grid [][]int, i, j int, sb *strings.Builder, dir int) { + // 计算行数和列数 + m, n := len(grid), len(grid[0]) + + // 如果(i, j)不在网格范围内, 或者是海洋, 直接返回 + if i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0 { + return + } + + // pre-order: 进入点(i, j) + grid[i][j] = 0 // 淹没该岛屿 + sb.WriteString(strconv.Itoa(dir) + ",") //记录方向 + + dfs(grid, i-1, j, sb, 1) // 上 + dfs(grid, i+1, j, sb, 2) // 下 + dfs(grid, i, j-1, sb, 3) // 左 + dfs(grid, i, j+1, sb, 4) // 右 + + // post-order: 离开点(i, j) + sb.WriteString(strconv.Itoa(-dir) + ",") // 记录方向 +} +``` + +```java +// by labuladong (java) +class Solution { + public int numDistinctIslands(int[][] grid) { + int m = grid.length, n = grid[0].length; + // 记录所有岛屿的序列化结果 + HashSet islands = new HashSet<>(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹掉这个岛屿,同时存储岛屿的序列化结果 + StringBuilder sb = new StringBuilder(); + // 初始的方向可以随便写,不影响正确性 + dfs(grid, i, j, sb, 666); + islands.add(sb.toString()); + /** + ![](../pictures/岛屿/6.png) + */ + } + } + } + // 不相同的岛屿数量 + return islands.size(); + } + + private void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n + || grid[i][j] == 0) { + return; + } + // 前序遍历位置:进入 (i, j) + grid[i][j] = 0; + sb.append(dir).append(','); + + dfs(grid, i - 1, j, sb, 1); // 上 + dfs(grid, i + 1, j, sb, 2); // 下 + dfs(grid, i, j - 1, sb, 3); // 左 + dfs(grid, i, j + 1, sb, 4); // 右 + + // 后序遍历位置:离开 (i, j) + sb.append(-dir).append(','); + } +} +``` + +```javascript +// by chatGPT (javascript) +var numDistinctIslands = function(grid) { + var m = grid.length, n = grid[0].length; + // Record the serialization results of all islands + var islands = new Set(); + for(var i=0; i + ![](../pictures/岛屿/6.png) + */ + } + } + } + // The number of different islands + return islands.size(); + + function dfs(grid, i, j, sb, dir) { + if(i<0 || j<0 || i>=m || j>=n || grid[i][j] == 0) { + return; + } + // Pre-order traversal position: enter (i, j) + grid[i][j] = 0; + sb.append(dir).append(','); + + dfs(grid, i-1, j, sb, 1); // up + dfs(grid, i+1, j, sb, 2); // down + dfs(grid, i, j-1, sb, 3); // left + dfs(grid, i, j+1, sb, 4); // right + + // Post-order traversal position: leave (i, j) + sb.append(-dir).append(','); + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def numDistinctIslands(self, grid: List[List[int]]) -> int: + m, n = len(grid), len(grid[0]) + islands = set() # 记录所有岛屿的序列化结果 + for i in range(m): + for j in range(n): + if grid[i][j] == 1: # 如果岛屿是1 + # 淹掉这个岛屿,同时存储岛屿的序列化结果 + sb = [] # 没有StringBuilder,用数组代替 + # 初始的方向可以随便写,不影响正确性 + self.dfs(grid, i, j, sb, 666) + islands.add("".join(sb)) + #![](../pictures/岛屿/6.png) + + return len(islands) # 返回不同的岛屿数量 + + def dfs(self, grid: List[List[int]], i: int, j: int, sb: List[str], dir: int) -> None: + m, n = len(grid), len(grid[0]) + if i < 0 or j < 0 or i >= m or j >= n or grid[i][j] == 0: + return + # 前序遍历位置:进入 (i, j) + grid[i][j] = 0 + sb.append(str(dir)).append(',') + + self.dfs(grid, i - 1, j, sb, 1) # 上 + self.dfs(grid, i + 1, j, sb, 2) # 下 + self.dfs(grid, i, j - 1, sb, 3) # 左 + self.dfs(grid, i, j + 1, sb, 4) # 右 + + # 后序遍历位置:离开 (i, j) + sb.append(str(-dir)).append(',') +``` + +https://leetcode.cn/problems/number-of-distinct-islands 的多语言解法👆 + +https://leetcode.cn/problems/number-of-enclaves 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int numEnclaves(vector>& grid) { + + int m = grid.size(), n = grid[0].size(); + + for (int i = 0; i < m; i++) { + dfs(grid, i, 0); + dfs(grid, i, n - 1); + } + + for (int j = 0; j < n; j++) { + dfs(grid, 0, j); + dfs(grid, m - 1, j); + } + + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + res += 1; + } + } + } + return res; + } + + void dfs(vector>& grid, int i, int j) { + int m = grid.size(), n = grid[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0) { + return; + } + + grid[i][j] = 0; + + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +}; +``` + +```go +// by chatGPT (go) +// LeetCode 题目链接 https://leetcode-cn.com/problems/number-of-enclaves/ +func numEnclaves(grid [][]int) int { + m, n := len(grid), len(grid[0]) + + for i := 0; i < m; i++ { + dfs(grid, i, 0) + dfs(grid, i, n-1) + } + + for j := 0; j < n; j++ { + dfs(grid, 0, j) + dfs(grid, m-1, j) + } + + var res int + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid[i][j] == 1 { + res += 1 + } + } + } + return res +} + +func dfs(grid [][]int, i, j int) { + m, n := len(grid), len(grid[0]) + if i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0 { + return + } + + grid[i][j] = 0 + + dfs(grid, i+1, j) + dfs(grid, i, j+1) + dfs(grid, i-1, j) + dfs(grid, i, j-1) +} +``` + +```java +// by labuladong (java) +class Solution { + public int numEnclaves(int[][] grid) { + int m = grid.length, n = grid[0].length; + + for (int i = 0; i < m; i++) { + dfs(grid, i, 0); + dfs(grid, i, n - 1); + } + + for (int j = 0; j < n; j++) { + dfs(grid, 0, j); + dfs(grid, m - 1, j); + } + + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + res += 1; + } + } + } + return res; + } + + void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0) { + return; + } + + grid[i][j] = 0; + + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} grid + * @return {number} + */ +var numEnclaves = function(grid) { + var m = grid.length, n = grid[0].length; + + for (var i = 0; i < m; i++) { + dfs(grid, i, 0); + dfs(grid, i, n - 1); + } + + for (var j = 0; j < n; j++) { + dfs(grid, 0, j); + dfs(grid, m - 1, j); + } + + var res = 0; + for (var row = 0; row < m; row++) { + for (var col = 0; col < n; col++) { + if (grid[row][col] === 1) { + res += 1; + } + } + } + return res; +}; + +function dfs(grid, i, j) { + var m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] === 0) { + return; + } + + grid[i][j] = 0; + + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); +} +``` + +```python +# by chatGPT (python) +class Solution: + def numEnclaves(self, grid: List[List[int]]) -> int: + m, n = len(grid), len(grid[0]) + + for i in range(m): + self.dfs(grid, i, 0) + self.dfs(grid, i, n - 1) + + for j in range(n): + self.dfs(grid, 0, j) + self.dfs(grid, m - 1, j) + + res = 0 + for i in range(m): + for j in range(n): + if grid[i][j] == 1: + res += 1 + return res + + def dfs(self, grid: List[List[int]], i: int, j: int) -> None: + m, n = len(grid), len(grid[0]) + if i < 0 or j < 0 or i >= m or j >= n or grid[i][j] == 0: + return + + grid[i][j] = 0 + + self.dfs(grid, i + 1, j) + self.dfs(grid, i, j + 1) + self.dfs(grid, i - 1, j) + self.dfs(grid, i, j - 1) +``` + +https://leetcode.cn/problems/number-of-enclaves 的多语言解法👆 + +https://leetcode.cn/problems/number-of-islands 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 主函数,计算岛屿数量 +public: + int numIslands(vector>& grid) { + int res = 0; + int m = grid.size(), n = grid[0].size(); + // 遍历 grid + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == '1') { + // 每发现一个岛屿,岛屿数量加一 + res++; + // 然后使用 DFS 将岛屿淹了 + dfs(grid, i, j); + } + } + } + return res; + } + +private: + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(vector>& grid, int i, int j) { + int m = grid.size(), n = grid[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + if (grid[i][j] == '0') { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = '0'; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +}; +``` + +```go +// by chatGPT (go) +func numIslands(grid [][]byte) int { + res := 0 + m, n := len(grid), len(grid[0]) + // 遍历 grid + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid[i][j] == '1' { + // 每发现一个岛屿,岛屿数量加一 + res++ + // 然后使用 DFS 将岛屿淹了 + dfs(grid, i, j) + } + } + } + return res +} + +// 从 (i, j) 开始,将与之相邻的陆地都变成海水 +func dfs(grid [][]byte, i, j int) { + m, n := len(grid), len(grid[0]) + if i < 0 || j < 0 || i >= m || j >= n { + // 超出索引边界 + return + } + if grid[i][j] == '0' { + // 已经是海水了 + return + } + // 将 (i, j) 变成海水 + grid[i][j] = '0' + // 淹没上下左右的陆地 + dfs(grid, i+1, j) + dfs(grid, i, j+1) + dfs(grid, i-1, j) + dfs(grid, i, j-1) +} +``` + +```java +// by labuladong (java) +class Solution { + // 主函数,计算岛屿数量 + public int numIslands(char[][] grid) { + int res = 0; + int m = grid.length, n = grid[0].length; + // 遍历 grid + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == '1') { + // 每发现一个岛屿,岛屿数量加一 + res++; + // 然后使用 DFS 将岛屿淹了 + dfs(grid, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(char[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + if (grid[i][j] == '0') { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = '0'; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +} +``` + +```javascript +// by chatGPT (javascript) +var numIslands = function(grid) { + var res = 0; + var m = grid.length, n = grid[0].length; + // 遍历 grid + for (var i = 0; i < m; i++) { + for (var j = 0; j < n; j++) { + if (grid[i][j] == '1') { + // 每发现一个岛屿,岛屿数量加一 + res++; + // 然后使用 DFS 将岛屿淹了 + dfs(grid, i, j); + } + } + } + return res; +}; + +// 从 (i, j) 开始,将与之相邻的陆地都变成海水 +function dfs(grid, i, j) { + var m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + if (grid[i][j] == '0') { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = '0'; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); +} +``` + +```python +# by chatGPT (python) +class Solution: + def numIslands(self, grid: List[List[str]]) -> int: + res = 0 + m = len(grid) + n = len(grid[0]) + # 遍历 grid + for i in range(m): + for j in range(n): + if grid[i][j] == '1': + # 每发现一个岛屿,岛屿数量加一 + res += 1 + # 然后使用 DFS 将岛屿淹了 + self.dfs(grid, i, j) + return res + + # 从 (i, j) 开始,将与之相邻的陆地都变成海水 + def dfs(self, grid: List[List[str]], i: int, j: int) -> None: + m = len(grid) + n = len(grid[0]) + if i < 0 or j < 0 or i >= m or j >= n: + # 超出索引边界 + return + if grid[i][j] == '0': + # 已经是海水了 + return + # 将 (i, j) 变成海水 + grid[i][j] = '0' + # 淹没上下左右的陆地 + self.dfs(grid, i + 1, j) + self.dfs(grid, i, j + 1) + self.dfs(grid, i - 1, j) + self.dfs(grid, i, j - 1) +``` + +https://leetcode.cn/problems/number-of-islands 的多语言解法👆 + +https://leetcode.cn/problems/number-of-students-unable-to-eat-lunch 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int countStudents(vector& students, vector& sandwiches) { + //studentCount [0 or 1] 分别代表吃 0 和吃 1的学生数量 + vector studentCount(2, 0); + for (int type : students) { + studentCount[type]++; + } + //遍历三明治栈,若栈顶的三明治无法被取走,则剩下的人都吃不上了 + for (int type : sandwiches) { + if (studentCount[type] == 0) { + //两种喜好加起来就是剩下的学生数量 + return studentCount[0] + studentCount[1]; + } + studentCount[type]--; + } + return 0; + } +}; +``` + +```go +// by chatGPT (go) +func countStudents(students []int, sandwiches []int) int { + // studentCount[0 or 1] 分别代表吃 0 和吃 1的学生数量 + studentCount := [2]int{0, 0} + for _, type_ := range students { + studentCount[type_]++ + } + // 遍历三明治栈,若栈顶的三明治无法被取走,则剩下的人都吃不上了 + for _, type_ := range sandwiches { + if studentCount[type_] == 0 { + // 两种喜好加起来就是剩下的学生数量 + return studentCount[0] + studentCount[1] + } + studentCount[type_]-- + } + return 0 +} +``` + +```java +// by labuladong (java) +class Solution { + public int countStudents(int[] students, int[] sandwiches) { + // studentCount[0 or 1] 分别代表吃 0 和吃 1的学生数量 + int[] studentCount = new int[2]; + for (int type : students) { + studentCount[type]++; + } + // 遍历三明治栈,若栈顶的三明治无法被取走,则剩下的人都吃不上了 + for (int type : sandwiches) { + if (studentCount[type] == 0) { + // 两种喜好加起来就是剩下的学生数量 + return studentCount[0] + studentCount[1]; + } + studentCount[type]--; + } + return 0; + } +} +``` + +```javascript +// by chatGPT (javascript) +var countStudents = function(students, sandwiches) { + // studentCount[0 or 1] 分别代表吃 0 和吃 1的学生数量 + var studentCount = new Array(2).fill(0); + for (var type of students) { + studentCount[type]++; + } + // 遍历三明治栈,若栈顶的三明治无法被取走,则剩下的人都吃不上了 + for (var type of sandwiches) { + if (studentCount[type] == 0) { + // 两种喜好加起来就是剩下的学生数量 + return studentCount[0] + studentCount[1]; + } + studentCount[type]--; + } + return 0; +} +``` + +```python +# by chatGPT (python) +class Solution: + def countStudents(self, students: List[int], sandwiches: List[int]) -> int: + # studentCount[0 or 1] 分别代表吃 0 和吃 1的学生数量 + studentCount = [0,0] + for type in students: + studentCount[type] += 1 + # 遍历三明治栈,若栈顶的三明治无法被取走,则剩下的人都吃不上了 + for type in sandwiches: + if studentCount[type] == 0: + # 两种喜好加起来就是剩下的学生数量 + return studentCount[0] + studentCount[1] + studentCount[type] -= 1 + return 0 +``` + +https://leetcode.cn/problems/number-of-students-unable-to-eat-lunch 的多语言解法👆 + +https://leetcode.cn/problems/open-the-lock 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int openLock(vector& deadends, string target) { + // 记录需要跳过的死亡密码 + unordered_set deads(deadends.begin(), deadends.end()); + // 记录已经穷举过的密码,防止走回头路 + unordered_set visited; + queue q; + // 从起点开始启动广度优先搜索 + int step = 0; + q.push("0000"); + visited.insert("0000"); + + while (!q.empty()) { + int sz = q.size(); + /* 将当前队列中的所有节点向周围扩散 */ + for (int i = 0; i < sz; i++) { + string cur = q.front(); q.pop(); + + /* 判断是否到达终点 */ + if (deads.count(cur)) + continue; + if (cur == target) + return step; + + /* 将一个节点的未遍历相邻节点加入队列 */ + for (int j = 0; j < 4; j++) { + string up = plusOne(cur, j); + if (!visited.count(up)) { + q.push(up); + visited.insert(up); + } + string down = minusOne(cur, j); + if (!visited.count(down)) { + q.push(down); + visited.insert(down); + } + } + } + /* 在这里增加步数 */ + step++; + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1; + } + + // 将 s[j] 向上拨动一次 + string plusOne(string s, int j) { + if (s[j] == '9') s[j] = '0'; + else s[j] += 1; + return s; + } + + // 将 s[i] 向下拨动一次 + string minusOne(string s, int j) { + if (s[j] == '0') s[j] = '9'; + else s[j] -= 1; + return s; + } +}; +``` + +```go +// by chatGPT (go) +import "fmt" + +func openLock(deadends []string, target string) int { + // 记录需要跳过的死亡密码 + deads := make(map[string]bool) + for _, s := range deadends { + deads[s] = true + } + // 记录已经穷举过的密码,防止走回头路 + visited := make(map[string]bool) + q := make([]string, 0) + // 从起点开始启动广度优先搜索 + step := 0 + q = append(q, "0000") + visited["0000"] = true + + for len(q) > 0 { + sz := len(q) + /* 将当前队列中的所有节点向周围扩散 */ + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + + /* 判断是否到达终点 */ + if deads[cur] { + continue + } + if cur == target { + return step + } + + /* 将一个节点的未遍历相邻节点加入队列 */ + for j := 0; j < 4; j++ { + up := plusOne(cur, j) + if !visited[up] { + q = append(q, up) + visited[up] = true + } + down := minusOne(cur, j) + if !visited[down] { + q = append(q, down) + visited[down] = true + } + } + } + /* 在这里增加步数 */ + step++ + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1 +} + +// 将 s[j] 向上拨动一次 +func plusOne(s string, j int) string { + ch := []byte(s) + if ch[j] == '9' { + ch[j] = '0' + } else { + ch[j] += 1 + } + return string(ch) +} + +// 将 s[i] 向下拨动一次 +func minusOne(s string, j int) string { + ch := []byte(s) + if ch[j] == '0' { + ch[j] = '9' + } else { + ch[j] -= 1 + } + return string(ch) +} +``` + +```java +// by labuladong (java) +class Solution { + public int openLock(String[] deadends, String target) { + // 记录需要跳过的死亡密码 + Set deads = new HashSet<>(); + for (String s : deadends) deads.add(s); + // 记录已经穷举过的密码,防止走回头路 + Set visited = new HashSet<>(); + Queue q = new LinkedList<>(); + // 从起点开始启动广度优先搜索 + int step = 0; + q.offer("0000"); + visited.add("0000"); + + while (!q.isEmpty()) { + int sz = q.size(); + /* 将当前队列中的所有节点向周围扩散 */ + for (int i = 0; i < sz; i++) { + String cur = q.poll(); + + /* 判断是否到达终点 */ + if (deads.contains(cur)) + continue; + if (cur.equals(target)) + return step; + + /* 将一个节点的未遍历相邻节点加入队列 */ + for (int j = 0; j < 4; j++) { + String up = plusOne(cur, j); + if (!visited.contains(up)) { + q.offer(up); + visited.add(up); + } + String down = minusOne(cur, j); + if (!visited.contains(down)) { + q.offer(down); + visited.add(down); + } + } + } + /* 在这里增加步数 */ + step++; + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1; + } + + // 将 s[j] 向上拨动一次 + String plusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '9') + ch[j] = '0'; + else + ch[j] += 1; + return new String(ch); + } + + // 将 s[i] 向下拨动一次 + String minusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '0') + ch[j] = '9'; + else + ch[j] -= 1; + return new String(ch); + } +} +``` + +```javascript +// by chatGPT (javascript) +var openLock = function(deadends, target) { + // 记录需要跳过的死亡密码 + const deads = new Set(deadends); + // 记录已经穷举过的密码,防止走回头路 + const visited = new Set(); + const q = []; + // 从起点开始启动广度优先搜索 + let step = 0; + q.push("0000"); + visited.add("0000"); + + while (q.length > 0) { + let sz = q.length; + /* 将当前队列中的所有节点向周围扩散 */ + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + /* 判断是否到达终点 */ + if (deads.has(cur)) continue; + if (cur === target) return step; + + /* 将一个节点的未遍历相邻节点加入队列 */ + for (let j = 0; j < 4; j++) { + let up = plusOne(cur, j); + if (!visited.has(up)) { + q.push(up); + visited.add(up); + } + let down = minusOne(cur, j); + if (!visited.has(down)) { + q.push(down); + visited.add(down); + } + } + } + /* 在这里增加步数 */ + step++; + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1; +} + +// 将 s[j] 向上拨动一次 +function plusOne(s, j) { + let ch = s.split(""); + if (ch[j] === "9") ch[j] = "0"; + else ch[j] = String(+ch[j]+1); + return ch.join(""); +} + +// 将 s[i] 向下拨动一次 +function minusOne(s, j) { + let ch = s.split(""); + if (ch[j] === "0") ch[j] = "9"; + else ch[j] = String(+ch[j]-1); + return ch.join(""); +} +``` + +```python +# by chatGPT (python) +class Solution: + def openLock(self, deadends: List[str], target: str) -> int: + # 记录需要跳过的死亡密码 + deads = set(deadends) + # 记录已经穷举过的密码,防止走回头路 + visited = set() + q = collections.deque() + # 从起点开始启动广度优先搜索 + step = 0 + q.append("0000") + visited.add("0000") + + while q: + sz = len(q) + # 将当前队列中的所有节点向周围扩散 + for i in range(sz): + cur = q.popleft() + + # 判断是否到达终点 + if cur in deads: + continue + if cur == target: + return step + + # 将一个节点的未遍历相邻节点加入队列 + for j in range(4): + up = plusOne(cur,j) + if up not in visited: + q.append(up) + visited.add(up) + down = minusOne(cur,j) + if down not in visited: + q.append(down) + visited.add(down) + # 在这里增加步数 + step += 1 + # 如果穷举完都没找到目标密码,那就是找不到了 + return -1 + + # 将 s[j] 向上拨动一次 + def plusOne(s: str, j: int) -> str: + ch = list(s) + if ch[j] == '9': + ch[j] = '0' + else: + ch[j] = chr(ord(ch[j])+1) + return "".join(ch) + + # 将 s[i] 向下拨动一次 + def minusOne(s: str, j: int) -> str: + ch = list(s) + if ch[j] == '0': + ch[j] = '9' + else: + ch[j] = chr(ord(ch[j])-1) + return "".join(ch) +``` + +https://leetcode.cn/problems/open-the-lock 的多语言解法👆 + +https://leetcode.cn/problems/out-of-boundary-paths 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + int memo[50][50][51]; + int m, n; +public: + //定义一个记录出界路径数量的函数 + int findPaths(int m, int n, int maxMove, int startRow, int startColumn) { + this->m = m; + this->n = n; + // 备忘录初始化为特殊值 -1 + memset(memo, -1, sizeof(memo)); + // 按照定义,计算从 (startRow, startColumn) 开始,最大移动次数为 maxMove 的出界路径数量 + return dp(maxMove, startRow, startColumn); + } + + //从 (i,j) 出发,最大移动次数为 maxMove,出界的路径数量为 dp(maxMove, i, j) + int dp(int maxMove, int i, int j) { + // base case,找到一条出界路径 + if (i < 0 || j < 0 || i >= m || j >= n) { + return 1; + } + // 避免冗余计算 + if (memo[i][j][maxMove] != -1) { + return memo[i][j][maxMove]; + } + // 无法在有限的步数内出界 + if (maxMove == 0) { + return 0; + } + // 状态转移关系: + // 在 maxMove 步之内从 (i, j) 踢出界的路径数量等于 + // 在 maxMove - 1 步之内从 (i, j) 的相邻位置踢出界的路径数量之和 + + long res = 0; + res += dp(maxMove - 1, i, j + 1); //向右走 + res += dp(maxMove - 1, i + 1, j); //向下走 + res += dp(maxMove - 1, i - 1, j); //向上走 + res += dp(maxMove - 1, i, j - 1); //向左走 + + memo[i][j][maxMove] = (int) (res % 1000000007); // 保存结果 + return memo[i][j][maxMove]; + } +}; +``` + +```go +// by chatGPT (go) +// 定义一个三维备忘录 +func findPaths(m int, n int, maxMove int, startRow int, startColumn int) int { + memo := make([][][]int, m) + for i := 0; i < m; i++ { + memo[i] = make([][]int, n) + for j := 0; j < n; j++ { + memo[i][j] = make([]int, maxMove+1) + for k := 0; k <= maxMove; k++ { + // 备忘录初始化为 -1 + memo[i][j][k] = -1 + } + } + } + + // 按照定义,计算从 (startRow, startColumn) 开始,最大移动次数为 maxMove 的出界路径数量 + return dp(memo, maxMove, startRow, startColumn, m, n) +} + +// 定义:从 (i, j) 出发,最大移动次数为 maxMove,出界的路径数量为 dp(memo, maxMove, i, j) +func dp(memo [][][]int, maxMove int, i int, j int, m int, n int) int { + // base case,找到一条出界路径 + if i < 0 || j < 0 || i >= m || j >= n { + return 1 + } + // 避免冗余计算 + if memo[i][j][maxMove] != -1 { + return memo[i][j][maxMove] + } + // 无法在有限的步数内出界 + if maxMove == 0 { + return 0 + } + // 状态转移关系: + // 在 maxMove 步之内从 (i, j) 踢出界的路径数量等于 + // 在 maxMove - 1 步之内从 (i, j) 的相邻位置踢出界的路径数量之和 + var res, mod int + mod = 1e9 + 7 + res += dp(memo, maxMove-1, i, j+1, m, n) + res += dp(memo, maxMove-1, i+1, j, m, n) + res += dp(memo, maxMove-1, i-1, j, m, n) + res += dp(memo, maxMove-1, i, j-1, m, n) + + memo[i][j][maxMove] = res % mod + return memo[i][j][maxMove] +} +``` + +```java +// by labuladong (java) +class Solution { + int[][][] memo; + int m, n; + + public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) { + this.m = m; + this.n = n; + memo = new int[m][n][maxMove + 1]; + // 备忘录初始化为特殊值 -1 + for (int[][] plane : memo) { + for (int[] row : plane) { + Arrays.fill(row, -1); + } + } + // 按照定义,计算从 (startRow, startColumn) 开始,最大移动次数为 maxMove 的出界路径数量 + return dp(maxMove, startRow, startColumn); + } + + // 定义:从 (i, j) 出发,最大移动次数为 maxMove,出界的路径数量为 dp(maxMove, i, j) + int dp(int maxMove, int i, int j) { + // base case,找到一条出界路径 + if (i < 0 || j < 0 || i >= m || j >= n) { + return 1; + } + // 避免冗余计算 + if (memo[i][j][maxMove] != -1) { + return memo[i][j][maxMove]; + } + // 无法在有限的步数内出界 + if (maxMove == 0) { + return 0; + } + // 状态转移关系: + // 在 maxMove 步之内从 (i, j) 踢出界的路径数量等于 + // 在 maxMove - 1 步之内从 (i, j) 的相邻位置踢出界的路径数量之和 + long res = 0; + res += dp(maxMove - 1, i, j + 1); + res += dp(maxMove - 1, i + 1, j); + res += dp(maxMove - 1, i - 1, j); + res += dp(maxMove - 1, i, j - 1); + + memo[i][j][maxMove] = (int) (res % 1000000007); + return memo[i][j][maxMove]; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} m + * @param {number} n + * @param {number} maxMove + * @param {number} startRow + * @param {number} startColumn + * @return {number} + */ +var findPaths = function(m, n, maxMove, startRow, startColumn) { + // memo数组用于存储计算过的结果,避免重复计算 + let memo = new Array(m); + for (let i = 0; i < m; i++) { + memo[i] = new Array(n); + for (let j = 0; j < n; j++) { + memo[i][j] = new Array(maxMove + 1).fill(-1); + } + } + // 按照定义,计算从(startRow, startColumn)开始,最大移动次数为maxMove的出界路径数量 + return dp(maxMove, startRow, startColumn); + + // 定义:从(i, j)出发,最大移动次数为maxMove,出界的路径数量为dp(maxMove, i, j) + function dp(maxMove, i, j) { + // base case,找到一条出界路径 + if (i < 0 || j < 0 || i >= m || j >= n) { + return 1; + } + // 避免冗余计算 + if (memo[i][j][maxMove] !== -1) { + return memo[i][j][maxMove]; + } + // 无法在有限的步数内出界 + if (maxMove === 0) { + return 0; + } + // 状态转移关系: + // 在 maxMove 步之内从 (i, j) 踢出界的路径数量等于 + // 在 maxMove - 1 步之内从 (i, j) 的相邻位置踢出界的路径数量之和 + let res = 0; + res += dp(maxMove - 1, i, j + 1); + res += dp(maxMove - 1, i + 1, j); + res += dp(maxMove - 1, i - 1, j); + res += dp(maxMove - 1, i, j - 1); + + memo[i][j][maxMove] = res % 1000000007; + return memo[i][j][maxMove]; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int: + memo = [[[-1 for _ in range(maxMove + 1)] for _ in range(n)] for _ in range(m)] + + def dp(maxMove: int, i: int, j: int) -> int: + # base case,找到一条出界路径 + if i < 0 or j < 0 or i >= m or j >= n: + return 1 + # 避免冗余计算 + if memo[i][j][maxMove] != -1: + return memo[i][j][maxMove] + # 无法在有限的步数内出界 + if maxMove == 0: + return 0 + # 状态转移关系: + # 在 maxMove 步之内从 (i, j) 踢出界的路径数量等于 + # 在 maxMove - 1 步之内从 (i, j) 的相邻位置踢出界的路径数量之和 + res = 0 + res += dp(maxMove - 1, i, j + 1) + res += dp(maxMove - 1, i + 1, j) + res += dp(maxMove - 1, i - 1, j) + res += dp(maxMove - 1, i, j - 1) + + memo[i][j][maxMove] = res % 1000000007 + return memo[i][j][maxMove] + + # 备忘录初始化为特殊值 -1 + for plane in memo: + for row in plane: + for k in range(maxMove + 1): + row[k] = -1 + + # 按照定义,计算从 (startRow, startColumn) 开始,最大移动次数为 maxMove 的出界路径数量 + return dp(maxMove, startRow, startColumn) +``` + +https://leetcode.cn/problems/out-of-boundary-paths 的多语言解法👆 + +https://leetcode.cn/problems/palindrome-linked-list 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isPalindrome(ListNode* head) { + ListNode *slow, *fast; // Define two pointers + slow = fast = head; // Initialize the pointers + while (fast != nullptr && fast->next != nullptr) { // Loop until fast pointer reaches the end + slow = slow->next; // Move slow pointer one step + fast = fast->next->next; // Move fast pointer two step + } + + if (fast != nullptr) // If fast pointer is not nullptr + slow = slow->next; // Move the slow pointer one step + + ListNode *left = head; // Initialize left pointer to head + ListNode *right = reverse(slow); // Reverse the right half of the list and make the right pointer point to the new head + while (right != nullptr) { // Loop until right pointer is nullptr + if (left->val != right->val) // If the values of left and right pointers are not equal + return false; // Return false + left = left->next; // Move the left pointer one step + right = right->next; // Move the right pointer one step + } + + return true; // Return true if all elements in the list are equal to their corresponding elements in the reversed list + } + + ListNode* reverse(ListNode* head) { + ListNode *pre = nullptr, *cur = head; // Define two pointers + while (cur != nullptr) { // Loop until the last node of the list is reached + ListNode *next = cur->next; // Store the next pointer temporarily + cur->next = pre; // Reverse the pointer + pre = cur; // Move the pre pointer one step + cur = next; // Move the cur pointer one step forward + } + return pre; // Return the new head + } +}; +``` + +```go +// by chatGPT (go) +func isPalindrome(head *ListNode) bool { + slow, fast := head, head + for fast != nil && fast.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + + if fast != nil { + slow = slow.Next + } + + left, right := head, reverse(slow) + for right != nil { + if left.Val != right.Val { + return false + } + left = left.Next + right = right.Next + } + + return true +} + +func reverse(head *ListNode) *ListNode { + var pre, cur *ListNode + cur = head + for cur != nil { + next := cur.Next + cur.Next = pre + pre = cur + cur = next + } + return pre +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isPalindrome(ListNode head) { + ListNode slow, fast; + slow = fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + if (fast != null) + slow = slow.next; + + ListNode left = head; + ListNode right = reverse(slow); + while (right != null) { + if (left.val != right.val) + return false; + left = left.next; + right = right.next; + } + + return true; + } + + ListNode reverse(ListNode head) { + ListNode pre = null, cur = head; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isPalindrome = function(head) { + let [slow, fast] = [head, head]; + while (fast !== null && fast.next !== null) { + slow = slow.next; + fast = fast.next.next; + } + + if (fast !== null) + slow = slow.next; + + let left = head; + let right = reverse(slow); + while (right !== null) { + if (left.val !== right.val) + return false; + left = left.next; + right = right.next; + } + + return true; +}; + +function reverse(head) { + let [pre, cur] = [null, head]; + while (cur !== null) { + let next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; +} +``` + +```python +# by chatGPT (python) +class Solution: + def isPalindrome(self, head: ListNode) -> bool: + slow, fast = head, head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + + if fast: + slow = slow.next + + left = head + right = self.reverse(slow) + while right: + if left.val != right.val: + return False + left = left.next + right = right.next + + return True + + def reverse(self, head: ListNode) -> ListNode: + pre, cur = None, head + while cur: + next_node = cur.next + cur.next = pre + pre = cur + cur = next_node + + return pre +``` + +https://leetcode.cn/problems/palindrome-linked-list 的多语言解法👆 + +https://leetcode.cn/problems/palindrome-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isPalindrome(int x) { + if (x < 0) { + return false; + } + int temp = x; + // y 是 x 翻转后的数字 + int y = 0; + while (temp > 0) { + int last_num = temp % 10; + temp = temp / 10; + // 从最高位生成数字的技巧 + y = y * 10 + last_num; + } + return y == x; + } +}; +``` + +```go +// by chatGPT (go) +// IsPalindrome 是判断一个整数是否为回文数的函数 +func IsPalindrome(x int) bool { + // 如果 x 是负数,那么它不可能为回文数,直接返回 false + if x < 0 { + return false + } + + // temp 是 x 的副本 + temp := x + // y 是 x 翻转后的数字 + y := 0 + + // 将 temp 逆序生成 y + for temp > 0 { + lastNum := temp % 10 + temp = temp / 10 + y = y*10 + lastNum + } + + // 如果 x 和 y 相等,那么 x 就是回文数 + return y == x +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isPalindrome(int x) { + if (x < 0) { + return false; + } + int temp = x; + // y 是 x 翻转后的数字 + int y = 0; + while (temp > 0) { + int last_num = temp % 10; + temp = temp / 10; + // 从最高位生成数字的技巧 + y = y * 10 + last_num; + } + return y == x; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} x + * @return {boolean} + */ +var isPalindrome = function(x) { + if (x < 0) { + return false; + } + var temp = x; + // y 是 x 翻转后的数字 + var y = 0; + while (temp > 0) { + var last_num = temp % 10; + temp = Math.floor(temp / 10); + // 从最高位生成数字的技巧 + y = y * 10 + last_num; + } + return y == x; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isPalindrome(self, x: int) -> bool: + if x < 0: + return False + temp = x + # y 是 x 翻转后的数字 + y = 0 + while temp > 0: + last_num = temp % 10 + temp = temp // 10 + # 从最高位生成数字的技巧 + y = y * 10 + last_num + return y == x +``` + +https://leetcode.cn/problems/palindrome-number 的多语言解法👆 + +https://leetcode.cn/problems/pancake-sorting 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + vector res; + +public: + vector pancakeSort(vector& cakes) { + sort(cakes, cakes.size()); + return res; + } + + void sort(vector& cakes, int n) { + // base case + if (n == 1) return; + + // 寻找最大饼的索引 + int maxCake = 0; + int maxCakeIndex = 0; + for (int i = 0; i < n; i++) + if (cakes[i] > maxCake) { + maxCakeIndex = i; + maxCake = cakes[i]; + } + + // 第一次翻转,将最大饼翻到最上面 + reverse(cakes, 0, maxCakeIndex); + res.push_back(maxCakeIndex + 1); + // 第二次翻转,将最大饼翻到最下面 + reverse(cakes, 0, n - 1); + res.push_back(n); + //递归调用 + sort(cakes, n - 1); + } + + void reverse(vector& arr, int i, int j) { + while (i < j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ + +func pancakeSort(cakes []int) []int { + // 记录反转操作序列 + res := []int{} + sort(cakes, len(cakes), &res) + return res +} + +func sort(cakes []int, n int, res *[]int) { + // base case + if n == 1 { + return + } + + // 寻找最大饼的索引 + maxCake := 0 + maxCakeIndex := 0 + for i := 0; i < n; i++ { + if cakes[i] > maxCake { + maxCakeIndex = i + maxCake = cakes[i] + } + } + + // 第一次翻转,将最大饼翻到最上面 + reverse(cakes, 0, maxCakeIndex) + *res = append(*res, maxCakeIndex+1) + // 第二次翻转,将最大饼翻到最下面 + reverse(cakes, 0, n-1) + *res = append(*res, n) + + // 递归调用 + sort(cakes, n-1, res) +} + +func reverse(arr []int, i int, j int) { + for i < j { + temp := arr[i] + arr[i] = arr[j]; + arr[j] = temp; + i++ + j-- + } +} +``` + +```java +// by labuladong (java) +class Solution { + // 记录反转操作序列 + LinkedList res = new LinkedList<>(); + + public List pancakeSort(int[] cakes) { + sort(cakes, cakes.length); + return res; + } + + void sort(int[] cakes, int n) { + // base case + if (n == 1) return; + + // 寻找最大饼的索引 + int maxCake = 0; + int maxCakeIndex = 0; + for (int i = 0; i < n; i++) + if (cakes[i] > maxCake) { + maxCakeIndex = i; + maxCake = cakes[i]; + } + + // 第一次翻转,将最大饼翻到最上面 + reverse(cakes, 0, maxCakeIndex); + res.add(maxCakeIndex + 1); + // 第二次翻转,将最大饼翻到最下面 + reverse(cakes, 0, n - 1); + res.add(n); + /** + ![](../pictures/pancakeSort/3.jpg) + */ + // 递归调用 + sort(cakes, n - 1); + /** + ![](../pictures/pancakeSort/4.jpg) + */ + } + + void reverse(int[] arr, int i, int j) { + while (i < j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var pancakeSort = function(cakes) { + // Record the flip sequence + // 记录反转操作序列 + const res = []; + + // Sort the cakes array and record the flipping sequence + // 对蛋糕数组进行排序,并记录翻转序列 + sort(cakes, cakes.length); + + // Return the flipping sequence + // 返回翻转序列 + return res; + + /** + * Sort the cakes array in decreasing order recursively + * 递归以递减的顺序对蛋糕数组排序 + * @param {number[]} cakes - The array for sorting + * @param {number} n - The length of the array + */ + function sort(cakes, n) { + // Base case + // 基本情况 + if (n == 1) return; + + // Find the index of max cake + // 找到最大蛋糕的索引 + let maxCake = 0; + let maxCakeIndex = 0; + + for (let i = 0; i < n; i++) { + if (cakes[i] > maxCake) { + maxCakeIndex = i; + maxCake = cakes[i]; + } + } + + // First flip to bring the max cake to the top + // 第一次翻转,将最大饼翻到最上面 + reverse(cakes, 0, maxCakeIndex); + + res.push(maxCakeIndex + 1); + + // Second flip the max cake to bring it to the bottom + // 第二次翻转,将最大饼翻到最下面 + reverse(cakes, 0, n - 1); + + res.push(n); + + // Recursively call the sort function with length-1 + // 递归调用长度减1的 sort 函数 + sort(cakes, n - 1); + } + + /** + * Flip the cake array from index i to index j + * 翻转蛋糕数组从 i 到 j 索引 + * @param {number[]} arr - The array for flipping + * @param {number} i - The start index + * @param {number} j - The end index + */ + function reverse(arr, i, j) { + while (i < j) { + let temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] #记录反转操作序列 + def pancakeSort(self, cakes: List[int]) -> List[int]: + self.sort(cakes, len(cakes)) + return self.res + + def sort(self, cakes: List[int], n: int) -> None: + # base case + if n == 1: + return + + # 寻找最大饼的索引 + maxCake = 0 + maxCakeIndex = 0 + for i in range(n): + if cakes[i] > maxCake: + maxCakeIndex = i + maxCake = cakes[i] + + # 第一次翻转,将最大饼翻到最上面 + self.reverse(cakes, 0, maxCakeIndex) + self.res.append(maxCakeIndex + 1) + # 第二次翻转,将最大饼翻到最下面 + self.reverse(cakes, 0, n - 1) + self.res.append(n) + + #递归调用 + self.sort(cakes, n - 1) + + def reverse(self, arr: List[int], i: int, j: int) -> None: + while i < j: + arr[i], arr[j] = arr[j], arr[i] + i += 1 + j -= 1 +``` + +https://leetcode.cn/problems/pancake-sorting 的多语言解法👆 + +https://leetcode.cn/problems/partition-equal-subset-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool canPartition(vector& nums) { + int sum = 0; + for (int num : nums) sum += num; + // 和为奇数时,不可能划分成两个和相等的集合 + if (sum % 2 != 0) return false; + int n = nums.size(); + sum = sum / 2; + vector> dp(n + 1, vector(sum + 1, false)); + // base case + for (int i = 0; i <= n; i++) + dp[i][0] = true; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= sum; j++) { + if (j - nums[i - 1] < 0) { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j]; + } else { + // 装入或不装入背包 + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; + } + } + } + return dp[n][sum]; + } +}; +``` + +```go +// by chatGPT (go) +func canPartition(nums []int) bool { + var sum int + for _, num := range nums { + sum += num + } + // 和为奇数时,不可能划分成两个和相等的集合 + if sum % 2 != 0 { + return false + } + n := len(nums) + sum = sum / 2 + dp := make([][]bool, n+1) + for i := range dp { + dp[i] = make([]bool, sum+1) + } + // base case + for i := 0; i <= n; i++ { + dp[i][0] = true + } + + for i := 1; i <= n; i++ { + for j := 1; j <= sum; j++ { + if j - nums[i-1] < 0 { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j] + } else { + // 装入或不装入背包 + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]] + } + } + } + return dp[n][sum] +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean canPartition(int[] nums) { + int sum = 0; + for (int num : nums) sum += num; + // 和为奇数时,不可能划分成两个和相等的集合 + if (sum % 2 != 0) return false; + int n = nums.length; + sum = sum / 2; + boolean[][] dp = new boolean[n + 1][sum + 1]; + // base case + for (int i = 0; i <= n; i++) + dp[i][0] = true; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= sum; j++) { + if (j - nums[i - 1] < 0) { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j]; + } else { + // 装入或不装入背包 + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; + } + } + } + return dp[n][sum]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var canPartition = function(nums) { + var sum = nums.reduce(function(acc, curr){return acc + curr}); + // 和为奇数时,不可能划分成两个和相等的集合 + if (sum % 2 !== 0) return false; + var n = nums.length; + sum = sum / 2; + var dp = new Array(n + 1).fill().map(() => new Array(sum + 1).fill(false)); + // base case + for (var i = 0; i <= n; i++) + dp[i][0] = true; + + for (var i = 1; i <= n; i++) { + for (var j = 1; j <= sum; j++) { + if (j - nums[i - 1] < 0) { + // 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j]; + } else { + // 装入或不装入背包 + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; + } + } + } + return dp[n][sum]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def canPartition(self, nums: List[int]) -> bool: + sum_ = sum(nums) + # 和为奇数时,不可能划分成两个和相等的集合 + if sum_ % 2 != 0: + return False + n = len(nums) + sum_ = sum_ // 2 + dp = [[False] * (sum_ + 1) for _ in range(n + 1)] + + # base case + for i in range(n + 1): + dp[i][0] = True + + for i in range(1, n + 1): + for j in range(1, sum_ + 1): + if j - nums[i - 1] < 0: + # 背包容量不足,不能装入第 i 个物品 + dp[i][j] = dp[i - 1][j] + else: + # 装入或不装入背包 + dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]] + return dp[n][sum_] +``` + +https://leetcode.cn/problems/partition-equal-subset-sum 的多语言解法👆 + +https://leetcode.cn/problems/partition-to-k-equal-sum-subsets 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool canPartitionKSubsets(vector& nums, int k) { + // 排除一些基本情况 + if (k > nums.size()) return false; + int sum = accumulate(nums.begin(), nums.end(), 0); + if (sum % k != 0) return false; + + int used = 0; + int target = sum / k; + return backtrack(k, 0, nums, 0, used, target); + } + +private: + unordered_map memo; + + bool backtrack(int k, int bucket, vector& nums, int start, int used, int target) { + if (k == 0) { + return true; + } + if (bucket == target) { + bool res = backtrack(k - 1, 0, nums, 0, used, target); + memo[used] = res; + return res; + } + + if (memo.count(used)) { + return memo[used]; + } + + for (int i = start; i < nums.size(); i++) { + if ((used >> i) & 1) { + continue; + } + if (nums[i] + bucket > target) { + continue; + } + used |= 1 << i; + bucket += nums[i]; + if (backtrack(k, bucket, nums, i + 1, used, target)) { + return true; + } + used ^= 1 << i; + bucket -= nums[i]; + } + + return false; + } +}; +``` + +```go +// by chatGPT (go) +func canPartitionKSubsets(nums []int, k int) bool { + memo := make(map[int]bool) + // 排除一些基本情况 + if k > len(nums) { + return false + } + sum := 0 + for _, v := range nums { + sum += v + } + if sum%k != 0 { + return false + } + + var backtrack func(k, bucket int, nums []int, start, used, target int) bool + backtrack = func(k, bucket int, nums []int, start, used, target int) bool { + // base case + if k == 0 { + // 所有桶都被装满了,而且 nums 一定全部用完了 + return true + } + if bucket == target { + // 装满了当前桶,递归穷举下一个桶的选择 + // 让下一个桶从 nums[0] 开始选数字 + if res, ok := memo[used]; ok { + return res + } + res := backtrack(k-1, 0, nums, 0, used, target) + // 缓存结果 + memo[used] = res + return res + } + + if res, ok := memo[used]; ok { + // 避免冗余计算 + return res + } + + for i := start; i < len(nums); i++ { + // 剪枝 + if (used>>i)&1 == 1 { // 判断第 i 位是否是 1 + // nums[i] 已经被装入别的桶中 + continue + } + if nums[i]+bucket > target { + continue + } + // 做选择 + newUsed := used | 1< nums.length) return false; + int sum = 0; + for (int v : nums) sum += v; + if (sum % k != 0) return false; + + int used = 0; // 使用位图技巧 + int target = sum / k; + // k 号桶初始什么都没装,从 nums[0] 开始做选择 + return backtrack(k, 0, nums, 0, used, target); + } + + HashMap memo = new HashMap<>(); + + boolean backtrack(int k, int bucket, + int[] nums, int start, int used, int target) { + // base case + if (k == 0) { + // 所有桶都被装满了,而且 nums 一定全部用完了 + return true; + } + if (bucket == target) { + // 装满了当前桶,递归穷举下一个桶的选择 + // 让下一个桶从 nums[0] 开始选数字 + boolean res = backtrack(k - 1, 0, nums, 0, used, target); + // 缓存结果 + memo.put(used, res); + return res; + } + + if (memo.containsKey(used)) { + // 避免冗余计算 + return memo.get(used); + } + + for (int i = start; i < nums.length; i++) { + // 剪枝 + if (((used >> i) & 1) == 1) { // 判断第 i 位是否是 1 + // nums[i] 已经被装入别的桶中 + continue; + } + if (nums[i] + bucket > target) { + continue; + } + // 做选择 + used |= 1 << i; // 将第 i 位置为 1 + bucket += nums[i]; + /** + ![](../pictures/集合划分/5.jpeg) + */ + // 递归穷举下一个数字是否装入当前桶 + if (backtrack(k, bucket, nums, i + 1, used, target)) { + return true; + } + // 撤销选择 + used ^= 1 << i; // 将第 i 位置为 0 + bucket -= nums[i]; + } + + return false; + } +} +``` + +```javascript +// by chatGPT (javascript) +var canPartitionKSubsets = function(nums, k) { + let memo = new Map(); + + function backtrack(k, bucket, nums, start, used, target) { + // base case + if (k == 0) { + // 所有桶都被装满了,而且 nums 一定全部用完了 + return true; + } + if (bucket == target) { + // 装满了当前桶,递归穷举下一个桶的选择 + // 让下一个桶从 nums[0] 开始选数字 + let res = backtrack(k - 1, 0, nums, 0, used, target); + // 缓存结果 + memo.set(used, res); + return res; + } + + if (memo.has(used)) { + // 避免冗余计算 + return memo.get(used); + } + + for (let i = start; i < nums.length; i++) { + // 剪枝 + if (((used >> i) & 1) == 1) { // 判断第 i 位是否是 1 + // nums[i] 已经被装入别的桶中 + continue; + } + if (nums[i] + bucket > target) { + continue; + } + // 做选择 + let newUsed = used | (1 << i); // 将第 i 位置为 1 + let newBucket = bucket + nums[i]; + /** + ![](../pictures/集合划分/5.jpeg) + */ + // 递归穷举下一个数字是否装入当前桶 + if (backtrack(k, newBucket, nums, i + 1, newUsed, target)) { + return true; + } + // 撤销选择 + newUsed ^= (1 << i); // 将第 i 位置为 0 + newBucket -= nums[i]; + } + + memo.set(used, false); + return false; + } + + // 排除一些基本情况 + if (k > nums.length) return false; + let sum = 0; + for (let v of nums) sum += v; + if (sum % k != 0) return false; + + let used = 0; // 使用位图技巧 + let target = sum / k; + // k 号桶初始什么都没装,从 nums[0] 开始做选择 + return backtrack(k, 0, nums, 0, used, target); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def canPartitionKSubsets(self, nums: List[int], k: int) -> bool: + # 排除一些基本情况 + if k > len(nums): + return False + _sum = sum(nums) + if _sum % k != 0: + return False + + used = 0 # 使用位图技巧 + target = _sum // k + # k 号桶初始什么都没装,从 nums[0] 开始做选择 + return self.backtrack(k, 0, nums, 0, used, target, {}) + + def backtrack(self, k: int, bucket: int, nums: List[int], start: int, used: int, target: int, memo: Dict[int, bool]) -> bool: + # base case + if k == 0: + # 所有桶都被装满了,而且 nums 一定全部用完了 + return True + if bucket == target: + # 装满了当前桶,递归穷举下一个桶的选择 + # 让下一个桶从 nums[0] 开始选数字 + if used in memo: + return memo[used] + res = self.backtrack(k - 1, 0, nums, 0, used, target, memo) + # 缓存结果 + memo[used] = res + return res + + for i in range(start, len(nums)): + # 剪枝 + if used >> i & 1 == 1: + # nums[i] 已经被装入别的桶中 + continue + if nums[i] + bucket > target: + continue + # 做选择 + used |= 1 << i # 将第 i 位置为 1 + bucket += nums[i] + # 递归穷举下一个数字是否装入当前桶 + if self.backtrack(k, bucket, nums, i + 1, used, target, memo): + return True + # 撤销选择 + used ^= 1 << i # 将第 i 位置为 0 + bucket -= nums[i] + + return False +``` + +https://leetcode.cn/problems/partition-to-k-equal-sum-subsets 的多语言解法👆 + +https://leetcode.cn/problems/pascals-triangle 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 迭代方式 +class Solution { +public: + vector> generate(int numRows) { + vector> res; + if (numRows < 1) { + return res; + } + // 先把第一层装进去作为 base case + vector firstRow(1, 1); + res.push_back(firstRow); + // 开始一层一层生成,装入 res + for (int i = 2; i <= numRows; i++) { + vector prevRow = res.back(); + res.push_back(generateNextRow(prevRow)); + } + return res; + } + + // 输入上一层的元素,生成并返回下一层的元素 + vector generateNextRow(vector& prevRow) { + vector curRow(1, 1); + for (int i = 0; i < prevRow.size() - 1; i++) { + curRow.push_back(prevRow[i] + prevRow[i + 1]); + } + curRow.push_back(1); + return curRow; + } +}; + +// 递归方式 +class Solution2 { +public: + // 定义:输入 numRows,返回行数为 numRows 的杨辉三角 + vector> generate(int numRows) { + // 递归的 base case + if (numRows == 1) { + vector> triangle(1, vector(1, 1)); + return triangle; + } + + // 先递归生成高度为 numRows - 1 的杨辉三角 + vector> triangle = generate(numRows - 1); + + // 根据最底层元素生成一行新元素 + vector bottomRow = triangle.back(); + vector newRow(1, 1); + for (int i = 0; i < bottomRow.size() - 1; i++) { + newRow.push_back(bottomRow[i] + bottomRow[i + 1]); + } + newRow.push_back(1); + // 把新的一行放到杨辉三角底部 + triangle.push_back(newRow); + + return triangle; + } +}; +``` + +```go +// by chatGPT (go) +// 迭代方式 +func generate(numRows int) [][]int { + res := [][]int{} + if numRows < 1 { + return res + } + // 先把第一层装进去作为 base case + firstRow := []int{1} + res = append(res, firstRow) + // 开始一层一层生成,装入 res + for i := 2; i <= numRows; i++ { + prevRow := res[len(res)-1] + res = append(res, generateNextRow(prevRow)) + } + return res +} + +// 输入上一层的元素,生成并返回下一层的元素 +func generateNextRow(prevRow []int) []int { + curRow := []int{1} + for i := 0; i < len(prevRow)-1; i++ { + curRow = append(curRow, prevRow[i]+prevRow[i+1]) + } + curRow = append(curRow, 1) + return curRow +} + +// 递归方式 +func generate(numRows int) [][]int { + // 递归的 base case + if numRows == 1 { + triangle := [][]int{} + // 先把第一层装进去作为 base case + firstRow := []int{1} + triangle = append(triangle, firstRow) + return triangle + } + + // 先递归生成高度为 numRows - 1 的杨辉三角 + triangle := generate(numRows - 1) + + // 根据最底层元素生成一行新元素 + bottomRow := triangle[len(triangle)-1] + newRow := []int{1} + for i := 0; i < len(bottomRow)-1; i++ { + newRow = append(newRow, bottomRow[i]+bottomRow[i+1]) + } + newRow = append(newRow, 1) + // 把新的一行放到杨辉三角底部 + triangle = append(triangle, newRow) + + return triangle +} +``` + +```java +// by labuladong (java) +// 迭代方式 +class Solution { + public List> generate(int numRows) { + List> res = new ArrayList<>(); + if (numRows < 1) { + return res; + } + // 先把第一层装进去作为 base case + List firstRow = new ArrayList<>(); + firstRow.add(1); + res.add(firstRow); + // 开始一层一层生成,装入 res + for (int i = 2; i <= numRows; i++) { + List prevRow = res.get(res.size() - 1); + res.add(generateNextRow(prevRow)); + } + return res; + } + + // 输入上一层的元素,生成并返回下一层的元素 + List generateNextRow(List prevRow) { + List curRow = new ArrayList<>(); + curRow.add(1); + for (int i = 0; i < prevRow.size() - 1; i++) { + curRow.add(prevRow.get(i) + prevRow.get(i + 1)); + } + curRow.add(1); + return curRow; + } +} + +// 递归方式 +class Solution2 { + // 定义:输入 numRows,返回行数为 numRows 的杨辉三角 + public List> generate(int numRows) { + // 递归的 base case + if (numRows == 1) { + List> triangle = new ArrayList<>(); + // 先把第一层装进去作为 base case + List firstRow = new ArrayList<>(); + firstRow.add(1); + triangle.add(firstRow); + return triangle; + } + + // 先递归生成高度为 numRows - 1 的杨辉三角 + List> triangle = generate(numRows - 1); + + // 根据最底层元素生成一行新元素 + List bottomRow = triangle.get(triangle.size() - 1); + List newRow = new ArrayList<>(); + newRow.add(1); + for (int i = 0; i < bottomRow.size() - 1; i++) { + newRow.add(bottomRow.get(i) + bottomRow.get(i + 1)); + } + newRow.add(1); + // 把新的一行放到杨辉三角底部 + triangle.add(newRow); + + return triangle; + } +} +``` + +```javascript +// by chatGPT (javascript) +var generate = function(numRows) { + var res = [] + if (numRows < 1) { + return res; + } + // 先把第一层装进去作为 base case + var firstRow = [1]; + res.push(firstRow); + // 开始一层一层生成,装入 res + for (var i = 2; i <= numRows; i++) { + var prevRow = res[res.length - 1]; + res.push(generateNextRow(prevRow)); + } + return res; +} + +// 输入上一层的元素,生成并返回下一层的元素 +var generateNextRow = function(prevRow) { + var curRow = [1]; + for (var i = 0; i < prevRow.length - 1; i++) { + curRow.push(prevRow[i] + prevRow[i + 1]); + } + curRow.push(1); + return curRow; +} + +// 定义:输入 numRows,返回行数为 numRows 的杨辉三角 +var generate = function(numRows) { + // 递归的 base case + if (numRows == 1) { + var triangle = []; + // 先把第一层装进去作为 base case + var firstRow = [1]; + triangle.push(firstRow); + return triangle; + } + + // 先递归生成高度为 numRows - 1 的杨辉三角 + var triangle = generate(numRows - 1); + + // 根据最底层元素生成一行新元素 + var bottomRow = triangle[triangle.length - 1]; + var newRow = [1]; + for (var i = 0; i < bottomRow.length - 1; i++) { + newRow.push(bottomRow[i] + bottomRow[i + 1]); + } + newRow.push(1); + // 把新的一行放到杨辉三角底部 + triangle.push(newRow); + + return triangle; +} +``` + +```python +# by chatGPT (python) +# 迭代方式 +class Solution: + def generate(self, numRows: int) -> List[List[int]]: + res = [] + if numRows < 1: + return res + # 先把第一层装进去作为 base case + firstRow = [1] + res.append(firstRow) + # 开始一层一层生成,装入 res + for i in range(2, numRows+1): + prevRow = res[-1] + res.append(self.generateNextRow(prevRow)) + return res + + # 输入上一层的元素,生成并返回下一层的元素 + def generateNextRow(self, prevRow: List[int]) -> List[int]: + curRow = [1] + for i in range(len(prevRow) - 1): + curRow.append(prevRow[i] + prevRow[i + 1]) + curRow.append(1) + return curRow + + +# 递归方式 +class Solution2: + # 定义:输入 numRows,返回行数为 numRows 的杨辉三角 + def generate(self, numRows: int) -> List[List[int]]: + # 递归的 base case + if numRows == 1: + triangle = [] + # 先把第一层装进去作为 base case + firstRow = [1] + triangle.append(firstRow) + return triangle + + # 先递归生成高度为 numRows - 1 的杨辉三角 + triangle = self.generate(numRows - 1) + + # 根据最底层元素生成一行新元素 + bottomRow = triangle[-1] + newRow = [1] + for i in range(len(bottomRow) - 1): + newRow.append(bottomRow[i] + bottomRow[i + 1]) + newRow.append(1) + # 把新的一行放到杨辉三角底部 + triangle.append(newRow) + + return triangle +``` + +https://leetcode.cn/problems/pascals-triangle 的多语言解法👆 + +https://leetcode.cn/problems/path-with-maximum-probability 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +#include +using namespace std; + +// struct 不再需要声明,直接在结构体内进行定义 +class Solution { +public: + double maxProbability(int n, vector>& edges, vector& succProb, int start, int end) { + vector>> graph(n); + for (int i = 0; i < edges.size(); i++) { + int from = edges[i][0]; + int to = edges[i][1]; + double weight = succProb[i]; + // 无向图其实就是双向图 + graph[from].push_back(make_pair(weight, to)); + graph[to].push_back(make_pair(weight, from)); + } + return dijkstra(start, end, graph); + } + + struct State { + int id; + double distFromStart; + State(int id, double distFromStart) : id(id), distFromStart(distFromStart) {} + bool operator < (const State& rhs) const { + return distFromStart < rhs.distFromStart; + } + }; + + double dijkstra(int start, int end, vector>>& graph) { + int V = graph.size(); + vector distTo(V, -1); + distTo[start] = 1; + priority_queue pq; + pq.push(State(start, 1)); + while (!pq.empty()) { + State curState = pq.top(); + pq.pop(); + int curNodeID = curState.id; + double curDistFromStart = curState.distFromStart; + if (curNodeID == end) { + return curDistFromStart; + } + if (curDistFromStart < distTo[curNodeID]) { + continue; + } + for (auto neighbor : graph[curNodeID]) { + int nextNodeID = neighbor.second; + double distToNextNode = distTo[curNodeID] * neighbor.first; + if (distTo[nextNodeID] < distToNextNode) { + distTo[nextNodeID] = distToNextNode; + pq.push(State(nextNodeID, distToNextNode)); + } + } + } + return 0.0; + } +}; +``` + +```go +// by chatGPT (go) +type State struct { + id int + dist float64 +} + +func maxProbability(n int, edges [][]int, succProb []float64, start int, end int) float64 { + graph := make([][]State, n) + for i := 0; i < n; i++ { + graph[i] = make([]State, 0) + } + + // 构造无向图 + for i := 0; i < len(edges); i++ { + from := edges[i][0] + to := edges[i][1] + weight := succProb[i] + // 无向图其实就是双向图 + graph[from] = append(graph[from], State{to, weight}) + graph[to] = append(graph[to], State{from, weight}) + } + + return dijkstra(start, end, graph) +} + +func dijkstra(start int, end int, graph [][]State) float64 { + // 图中节点的个数 + V := len(graph) + // 记录最短路径的权重,你可以理解为 dp table + // 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重 + distTo := make([]float64, V) + // dp table 初始化为正无穷 + for i := 0; i < V; i++ { + distTo[i] = -1 + } + // base case,start 到 start 的最短距离就是 0 + distTo[start] = 1 + + // 优先级队列,distFromStart 较小的排在前面 + pq := make(PriorityQueue, 0) + heap.Init(&pq) + //从起点 start 开始进行 BFS + heap.Push(&pq, &State{id: start, dist: 1}) + + for pq.Len() > 0 { + curState := heap.Pop(&pq).(*State) + curNodeID := curState.id + curDistFromStart := curState.dist + + // 在这里加一个判断就行了,其他代码不用改 + if curNodeID == end { + return curDistFromStart + } + + if curDistFromStart < distTo[curNodeID] { + // 已经有一条更短的路径到达 curNode 节点了 + continue + } + // 将 curNode 的相邻节点装入队列 + for _, neighbor := range graph[curNodeID] { + nextNodeID := neighbor.id + // 看看从 curNode 达到 nextNode 的距离是否会更短 + distToNextNode := distTo[curNodeID] * neighbor.dist + if distTo[nextNodeID] < distToNextNode { + // 更新 dp table + distTo[nextNodeID] = distToNextNode + // 将这个节点以及距离放入队列 + heap.Push(&pq, &State{nextNodeID, distToNextNode}) + } + } + } + return 0.0 +} + +// 优先级队列数据结构,用于实现 BFS 广度优先搜索 +type PriorityQueue []*State + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].dist > pq[j].dist +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(*State) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} +``` + +```java +// by labuladong (java) +class Solution { + public double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) { + List[] graph = new LinkedList[n]; + for (int i = 0; i < n; i++) { + graph[i] = new LinkedList<>(); + } + // 构造无向图 + for (int i = 0; i < edges.length; i++) { + int from = edges[i][0]; + int to = edges[i][1]; + double weight = succProb[i]; + // 无向图其实就是双向图 + graph[from].add(new double[]{(double)to, weight}); + graph[to].add(new double[]{(double)from, weight}); + } + + + return dijkstra(start, end, graph); + } + + class State { + // 图节点的 id + int id; + // 从 start 节点到当前节点的距离 + double distFromStart; + + State(int id, double distFromStart) { + this.id = id; + this.distFromStart = distFromStart; + } + } + + double dijkstra(int start, int end, List[] graph) { + // 图中节点的个数 + int V = graph.length; + // 记录最短路径的权重,你可以理解为 dp table + // 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重 + double[] distTo = new double[V]; + // dp table 初始化为正无穷 + Arrays.fill(distTo, -1); + // base case,start 到 start 的最短距离就是 0 + distTo[start] = 1; + + // 优先级队列,distFromStart 较小的排在前面 + Queue pq = new PriorityQueue<>((a, b) -> { + return Double.compare(b.distFromStart, a.distFromStart); + }); + // 从起点 start 开始进行 BFS + pq.offer(new State(start, 1)); + + while (!pq.isEmpty()) { + State curState = pq.poll(); + int curNodeID = curState.id; + double curDistFromStart = curState.distFromStart; + + // 在这里加一个判断就行了,其他代码不用改 + if (curNodeID == end) { + return curDistFromStart; + } + + if (curDistFromStart < distTo[curNodeID]) { + // 已经有一条更短的路径到达 curNode 节点了 + continue; + } + // 将 curNode 的相邻节点装入队列 + for (double[] neighbor : graph[curNodeID]) { + int nextNodeID = (int)neighbor[0]; + // 看看从 curNode 达到 nextNode 的距离是否会更短 + double distToNextNode = distTo[curNodeID] * neighbor[1]; + if (distTo[nextNodeID] < distToNextNode) { + // 更新 dp table + distTo[nextNodeID] = distToNextNode; + // 将这个节点以及距离放入队列 + pq.offer(new State(nextNodeID, distToNextNode)); + } + } + } + return 0.0; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @param {number[][]} edges + * @param {number[]} succProb + * @param {number} start + * @param {number} end + * @return {number} + */ +var maxProbability = function(n, edges, succProb, start, end) { + const graph = []; + for (let i = 0; i < n; i++) { + graph[i] = []; + } + // 构造无向图 + for (let i = 0; i < edges.length; i++) { + const from = edges[i][0]; + const to = edges[i][1]; + const weight = succProb[i]; + // 无向图其实就是双向图 + graph[from].push([to, weight]); + graph[to].push([from, weight]); + } + + + return dijkstra(start, end, graph); +}; + +class State { + // 图节点的 id + constructor(id, distFromStart) { + this.id = id; + // 从 start 节点到当前节点的距离 + this.distFromStart = distFromStart; + } +} + +function dijkstra(start, end, graph) { + // 图中节点的个数 + const V = graph.length; + // 记录最短路径的权重,你可以理解为 dp table + // 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重 + const distTo = new Array(V).fill(-1); + // dp table 初始化为正无穷 + distTo[start] = 1; + + // 优先级队列,distFromStart 较小的排在前面 + const pq = new PriorityQueue({ + compare: (a, b) => { + return b.distFromStart - a.distFromStart; + } + }); + // 从起点 start 开始进行 BFS + pq.enqueue(new State(start, 1)); + + while (!pq.isEmpty()) { + const curState = pq.dequeue(); + const curNodeID = curState.id; + const curDistFromStart = curState.distFromStart; + + // 在这里加一个判断就行了,其他代码不用改 + if (curNodeID === end) { + return curDistFromStart; + } + + if (curDistFromStart < distTo[curNodeID]) { + // 已经有一条更短的路径到达 curNode 节点了 + continue; + } + // 将 curNode 的相邻节点装入队列 + for (const neighbor of graph[curNodeID]) { + const nextNodeID = neighbor[0]; + // 看看从 curNode 达到 nextNode 的距离是否会更短 + const distToNextNode = distTo[curNodeID] * neighbor[1]; + if (distTo[nextNodeID] < distToNextNode) { + // 更新 dp table + distTo[nextNodeID] = distToNextNode; + // 将这个节点以及距离放入队列 + pq.enqueue(new State(nextNodeID, distToNextNode)); + } + } + } + return 0.0; +} +``` + +```python +# by chatGPT (python) +import heapq +from typing import List + + +class Solution: + def maxProbability(self, n: int, edges: List[List[int]], succProb: List[float], start: int, end: int) -> float: + graph = [[] for _ in range(n)] + for i in range(len(edges)): + from_, to = edges[i][0], edges[i][1] + weight = succProb[i] + graph[from_].append((to, weight)) + graph[to].append((from_, weight)) + + return self.dijkstra(start, end, graph) + + class State: + def __init__(self, id_, distFromStart): + self.id = id_ + self.distFromStart = distFromStart + + def __lt__(self, other): + return self.distFromStart > other.distFromStart + + def dijkstra(self, start, end, graph): + V = len(graph) + # 记录最短路径的权重,你可以理解为 dp table + # 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重 + distTo = [-1] * V + # dp table 初始化为正无穷 + distTo[start] = 1 + + # 优先级队列,distFromStart 较小的排在前面 + pq = [] + # 从起点 start 开始进行 BFS + heapq.heappush(pq, self.State(start, 1)) + + while pq: + curState = heapq.heappop(pq) + curNodeID = curState.id + curDistFromStart = curState.distFromStart + + # 在这里加一个判断就行了,其他代码不用改 + if curNodeID == end: + return curDistFromStart + + if curDistFromStart < distTo[curNodeID]: + # 已经有一条更短的路径到达 curNode 节点了 + continue + # 将 curNode 的相邻节点装入队列 + for neighbor in graph[curNodeID]: + nextNodeID = neighbor[0] + # 看看从 curNode 达到 nextNode 的距离是否会更短 + distToNextNode = distTo[curNodeID] * neighbor[1] + if distTo[nextNodeID] < distToNextNode: + # 更新 dp table + distTo[nextNodeID] = distToNextNode + # 将这个节点以及距离放入队列 + heapq.heappush(pq, self.State(nextNodeID, distToNextNode)) + return 0.0 +``` + +https://leetcode.cn/problems/path-with-maximum-probability 的多语言解法👆 + +https://leetcode.cn/problems/path-with-minimum-effort 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // Dijkstra 算法,计算 (0, 0) 到 (m - 1, n - 1) 的最小体力消耗 + int minimumEffortPath(vector>& heights) { + int m = heights.size(), n = heights[0].size(); + // 定义:从 (0, 0) 到 (i, j) 的最小体力消耗是 effortTo[i][j] + vector> effortTo(m,vector(n, INT_MAX)); // dp table 初始化为正无穷 + // base case,起点到起点的最小消耗就是 0 + effortTo[0][0] = 0; + + // 优先级队列,effortFromStart 较小的排在前面 + priority_queue,Comp> pq; // 第二个参数就是自定义语法格式,详见下方 + // 从起点 (0, 0) 开始进行 BFS + pq.push(State(0, 0, 0)); + + while (!pq.empty()) { + State curState = pq.top(); + pq.pop(); + int curX = curState.x; + int curY = curState.y; + int curEffortFromStart = curState.effortFromStart; + + // 到达终点提前结束 + if (curX == m - 1 && curY == n - 1) { + return curEffortFromStart; + } + + if (curEffortFromStart > effortTo[curX][curY]) { + continue; + } + // 将 (curX, curY) 的相邻坐标装入队列 + for (auto& neighbor : adj(heights, curX, curY)) { + int nextX = neighbor[0]; + int nextY = neighbor[1]; + // 计算从 (curX, curY) 达到 (nextX, nextY) 的消耗 + int effortToNextNode = max( + effortTo[curX][curY], + abs(heights[curX][curY] - heights[nextX][nextY]) + ); + // 更新 dp table + if (effortTo[nextX][nextY] > effortToNextNode) { + effortTo[nextX][nextY] = effortToNextNode; + pq.push(State(nextX, nextY, effortToNextNode)); + } + } + } + // 正常情况不会达到这个 return + return -1; + } + + // 方向数组,上下左右的坐标偏移量 + vector> dirs {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; + + // 返回坐标 (x, y) 的上下左右相邻坐标 + vector> adj(vector>& matrix, int x, int y) { + int m = matrix.size(), n = matrix[0].size(); + // 存储相邻节点 + vector> neighbors; + for (auto& dir : dirs) { + int nx = x + dir[0]; + int ny = y + dir[1]; + if (nx >= m || nx < 0 || ny >= n || ny < 0) { + // 索引越界 + continue; + } + neighbors.push_back({nx, ny}); + } + return neighbors; + } + + struct State { + // 矩阵中的一个位置 + int x, y; + // 从起点 (0, 0) 到当前位置的最小体力消耗(距离) + int effortFromStart; + + State(int x, int y, int effortFromStart) { + this->x = x; + this->y = y; + this->effortFromStart = effortFromStart; + } + }; + + struct Comp { + bool operator() (State& a, State& b) { + return a.effortFromStart > b.effortFromStart; + } + }; +}; +``` + +```go +// by chatGPT (go) +func minimumEffortPath(heights [][]int) int { + m := len(heights) + n := len(heights[0]) + // 定义:从 (0, 0) 到 (i, j) 的最小体力消耗是 effortTo[i][j] + effortTo := make([][]int, m) + for i := range effortTo { + effortTo[i] = make([]int, n) + // dp table 初始化为正无穷 + for j := range effortTo[i] { + effortTo[i][j] = math.MaxInt32 + } + } + // base case,起点到起点的最小消耗就是 0 + effortTo[0][0] = 0 + + // 方向数组,上下左右的坐标偏移量 + var dirs = [][2]int{{0, 1}, {1, 0}, {0, -1}, {-1, 0}} + + // 返回坐标 (x, y) 的上下左右相邻坐标 + var adj = func(x, y int) [][]int { + // 存储相邻节点 + neighbors := make([][]int, 0) + for _, dir := range dirs { + nx, ny := x+dir[0], y+dir[1] + if nx >= m || nx < 0 || ny >= n || ny < 0 { + // 索引越界 + continue + } + neighbors = append(neighbors, []int{nx, ny}) + } + return neighbors + } + + // 优先级队列的类型定义 + type PriorityQueue []*State + + func (pq PriorityQueue) Len() int { return len(pq) } + func (pq PriorityQueue) Less(i, j int) bool { return pq[i].effortFromStart < pq[j].effortFromStart } + func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] } + func (pq *PriorityQueue) Push(x interface{}) { *pq = append(*pq, x.(*State)) } + func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item + } + + // 从起点 (0, 0) 开始进行 BFS + pq := make(PriorityQueue, 0) + heap.Init(&pq) + pq.Push(&State{0, 0, 0}) + for pq.Len() > 0 { + curState := heap.Pop(&pq).(*State) + curX, curY, curEffortFromStart := curState.x, curState.y, curState.effortFromStart + + // 到达终点提前结束 + if curX == m-1 && curY == n-1 { + return curEffortFromStart + } + + if curEffortFromStart > effortTo[curX][curY] { + continue + } + // 将 (curX, curY) 的相邻坐标装入队列 + for _, neighbor := range adj(curX, curY) { + nextX, nextY := neighbor[0], neighbor[1] + // 计算从 (curX, curY) 达到 (nextX, nextY) 的消耗 + effortToNextNode := max( + effortTo[curX][curY], + abs(heights[curX][curY]-heights[nextX][nextY]), + ) + // 更新 dp table + if effortTo[nextX][nextY] > effortToNextNode { + effortTo[nextX][nextY] = effortToNextNode + heap.Push(&pq, &State{nextX, nextY, effortToNextNode}) + } + } + } + // 正常情况不会达到这个 return + return -1 +} + +type State struct { + // 矩阵中的一个位置 + x, y int + // 从起点 (0, 0) 到当前位置的最小体力消耗(距离) + effortFromStart int +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} +``` + +```java +// by labuladong (java) +class Solution { + // Dijkstra 算法,计算 (0, 0) 到 (m - 1, n - 1) 的最小体力消耗 + public int minimumEffortPath(int[][] heights) { + int m = heights.length, n = heights[0].length; + // 定义:从 (0, 0) 到 (i, j) 的最小体力消耗是 effortTo[i][j] + int[][] effortTo = new int[m][n]; + // dp table 初始化为正无穷 + for (int i = 0; i < m; i++) { + Arrays.fill(effortTo[i], Integer.MAX_VALUE); + } + // base case,起点到起点的最小消耗就是 0 + effortTo[0][0] = 0; + + // 优先级队列,effortFromStart 较小的排在前面 + Queue pq = new PriorityQueue<>((a, b) -> { + return a.effortFromStart - b.effortFromStart; + }); + + // 从起点 (0, 0) 开始进行 BFS + pq.offer(new State(0, 0, 0)); + + while (!pq.isEmpty()) { + State curState = pq.poll(); + int curX = curState.x; + int curY = curState.y; + int curEffortFromStart = curState.effortFromStart; + + // 到达终点提前结束 + if (curX == m - 1 && curY == n - 1) { + return curEffortFromStart; + } + + if (curEffortFromStart > effortTo[curX][curY]) { + continue; + } + // 将 (curX, curY) 的相邻坐标装入队列 + for (int[] neighbor : adj(heights, curX, curY)) { + int nextX = neighbor[0]; + int nextY = neighbor[1]; + // 计算从 (curX, curY) 达到 (nextX, nextY) 的消耗 + int effortToNextNode = Math.max( + effortTo[curX][curY], + Math.abs(heights[curX][curY] - heights[nextX][nextY]) + ); + // 更新 dp table + if (effortTo[nextX][nextY] > effortToNextNode) { + effortTo[nextX][nextY] = effortToNextNode; + pq.offer(new State(nextX, nextY, effortToNextNode)); + } + } + } + // 正常情况不会达到这个 return + return -1; + } + + // 方向数组,上下左右的坐标偏移量 + int[][] dirs = new int[][]{{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; + + // 返回坐标 (x, y) 的上下左右相邻坐标 + List adj(int[][] matrix, int x, int y) { + int m = matrix.length, n = matrix[0].length; + // 存储相邻节点 + List neighbors = new ArrayList<>(); + for (int[] dir : dirs) { + int nx = x + dir[0]; + int ny = y + dir[1]; + if (nx >= m || nx < 0 || ny >= n || ny < 0) { + // 索引越界 + continue; + } + neighbors.add(new int[]{nx, ny}); + } + return neighbors; + } + + class State { + // 矩阵中的一个位置 + int x, y; + // 从起点 (0, 0) 到当前位置的最小体力消耗(距离) + int effortFromStart; + + State(int x, int y, int effortFromStart) { + this.x = x; + this.y = y; + this.effortFromStart = effortFromStart; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var minimumEffortPath = function(heights) { + + // Dijkstra 算法,计算 (0, 0) 到 (m - 1, n - 1) 的最小体力消耗 + let m = heights.length, + n = heights[0].length, + // 定义:从 (0, 0) 到 (i, j) 的最小体力消耗是 effortTo[i][j] + effortTo = Array.from({ length: m }, () => Array(n).fill(Number.MAX_SAFE_INTEGER)), + // 方向数组,上下左右的坐标偏移量 + dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]], + // 优先级队列,effortFromStart 较小的排在前面 + pq = []; + + // 从起点 (0, 0) 开始进行 BFS + pq.push(new State(0, 0, 0)); + + // base case,起点到起点的最小消耗就是 0 + effortTo[0][0] = 0; + + class State { + // 矩阵中的一个位置 + // 从起点 (0, 0) 到当前位置的最小体力消耗(距离) + constructor(x, y, effortFromStart) { + this.x = x; + this.y = y; + this.effortFromStart = effortFromStart; + } + } + + function adj(matrix, x, y) { + let m = matrix.length, n = matrix[0].length; + // 存储相邻节点 + let neighbors = []; + for (let dir of dirs) { + let nx = x + dir[0]; + let ny = y + dir[1]; + if (nx >= m || nx < 0 || ny >= n || ny < 0) { + // 索引越界 + continue; + } + neighbors.push([nx, ny]); + } + return neighbors; + } + + while (pq.length != 0) { + let curState = pq.shift(); + let curX = curState.x; + let curY = curState.y; + let curEffortFromStart = curState.effortFromStart; + + // 到达终点提前结束 + if (curX == m - 1 && curY == n - 1) { + return curEffortFromStart; + } + + if (curEffortFromStart > effortTo[curX][curY]) { + continue; + } + + // 将 (curX, curY) 的相邻坐标装入队列 + for (let neighbor of adj(heights, curX, curY)) { + let nextX = neighbor[0]; + let nextY = neighbor[1]; + // 计算从 (curX, curY) 达到 (nextX, nextY) 的消耗 + let effortToNextNode = Math.max( + effortTo[curX][curY], + Math.abs(heights[curX][curY] - heights[nextX][nextY]) + ); + // 更新 dp table + if (effortTo[nextX][nextY] > effortToNextNode) { + effortTo[nextX][nextY] = effortToNextNode; + pq.push(new State(nextX, nextY, effortToNextNode)); + } + } + } + + // 正常情况不会达到这个 return + return -1; +}; +``` + +```python +# by chatGPT (python) +import heapq + +class Solution: + def minimumEffortPath(self, heights: List[List[int]]) -> int: + m, n = len(heights), len(heights[0]) + # 定义:从 (0, 0) 到 (i, j) 的最小体力消耗是 effortTo[i][j] + effortTo = [[float('inf')] * n for _ in range(m)] + # base case,起点到起点的最小消耗就是 0 + effortTo[0][0] = 0 + + # 优先级队列,effortFromStart 较小的排在前面 + pq = [] + + # 从起点 (0, 0) 开始进行 BFS + heapq.heappush(pq, [0, 0, 0]) + + while pq: + curEffortFromStart, curX, curY = heapq.heappop(pq) + + # 到达终点提前结束 + if curX == m - 1 and curY == n - 1: + return curEffortFromStart + + if curEffortFromStart > effortTo[curX][curY]: + continue + + # 将 (curX, curY) 的相邻坐标装入队列 + for nextX, nextY in self.adj(heights, curX, curY): + # 计算从 (curX, curY) 达到 (nextX, nextY) 的消耗 + effortToNextNode = max( + effortTo[curX][curY], + abs(heights[curX][curY] - heights[nextX][nextY]) + ) + # 更新 dp table + if effortTo[nextX][nextY] > effortToNextNode: + effortTo[nextX][nextY] = effortToNextNode + heapq.heappush(pq, [effortToNextNode, nextX, nextY]) + + # 正常情况不会达到这个 return + return -1 + + # 方向数组,上下左右的坐标偏移量 + dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]] + + # 返回坐标 (x, y) 的上下左右相邻坐标 + def adj(self, matrix, x, y): + m, n = len(matrix), len(matrix[0]) + # 存储相邻节点 + neighbors = [] + for dir in self.dirs: + nx = x + dir[0] + ny = y + dir[1] + if nx >= m or nx < 0 or ny >= n or ny < 0: + # 索引越界 + continue + neighbors.append([nx, ny]) + return neighbors +``` + +https://leetcode.cn/problems/path-with-minimum-effort 的多语言解法👆 + +https://leetcode.cn/problems/peak-index-in-a-mountain-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int peakIndexInMountainArray(vector& nums) { + // 取两端都闭的二分搜索 + int left = 0, right = nums.size() - 1; + // 因为题目必然有解,所以设置 left == right 为结束条件 + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] > nums[mid + 1]) { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid; + } else { + // mid 右侧有一个峰值 + left = mid + 1; + } + } + return left; + } +}; +``` + +```go +// by chatGPT (go) +func peakIndexInMountainArray(nums []int) int { + // 取两端都闭的二分搜索 + left, right := 0, len(nums)-1 + // 因为题目必然有解,所以设置 left == right 为结束条件 + for left < right { + mid := left + (right-left)/2 + if nums[mid] > nums[mid+1] { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid + } else { + // mid 右侧有一个峰值 + left = mid + 1 + } + } + return left +} +``` + +```java +// by labuladong (java) +class Solution { + public int peakIndexInMountainArray(int[] nums) { + // 取两端都闭的二分搜索 + int left = 0, right = nums.length - 1; + // 因为题目必然有解,所以设置 left == right 为结束条件 + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] > nums[mid + 1]) { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid; + } else { + // mid 右侧有一个峰值 + left = mid + 1; + } + } + return left; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number} + */ +var peakIndexInMountainArray = function(nums) { + // 取两端都闭的二分搜索 + let left = 0, right = nums.length - 1; + // 因为题目必然有解,所以设置 left == right 为结束条件 + while (left < right) { + const mid = left + Math.floor((right - left) / 2); + if (nums[mid] > nums[mid + 1]) { + // mid 本身就是峰值或其左侧有一个峰值 + right = mid; + } else { + // mid 右侧有一个峰值 + left = mid + 1; + } + } + return left; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def peakIndexInMountainArray(self, nums: List[int]) -> int: + # 取两端都闭的二分搜索 + left, right = 0, len(nums) - 1 + # 因为题目必然有解,所以设置 left == right 为结束条件 + while left < right: + mid = left + (right - left) // 2 + if nums[mid] > nums[mid + 1]: + # mid 本身就是峰值或其左侧有一个峰值 + right = mid + else: + # mid 右侧有一个峰值 + left = mid + 1 + return left +``` + +https://leetcode.cn/problems/peak-index-in-a-mountain-array 的多语言解法👆 + +https://leetcode.cn/problems/peeking-iterator 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class PeekingIterator : public Iterator { +private: + Iterator* iter; // 迭代器 + int nextElem; // 缓存下一个元素 +public: + PeekingIterator(const vector& nums) : Iterator(nums) { + iter = &nums; // 初始化迭代器 + if (Iterator::hasNext()) { // 检查是否有下一个元素 + nextElem = Iterator::next(); // 取出下一个元素并缓存 + } + } + + int peek() { // 返回当前缓存的元素 + return nextElem; + } + + int next() { // 返回缓存的下一个元素,并更新缓存 + int res = nextElem; + if (Iterator::hasNext()) { + nextElem = Iterator::next(); + } else { + nextElem = NULL; + } + return res; + } + + bool hasNext() const { // 检查是否有下一个元素 + return nextElem != NULL; + } +}; +``` + +```go +// by chatGPT (go) +type Iterator interface { + hasNext() bool + next() int +} + +type PeekingIterator struct { + iter Iterator + // 保存下一个元素的缓存 + nextElem int +} + +func Constructor(iter Iterator) *PeekingIterator { + this := new(PeekingIterator) + this.iter = iter + // 把下一个元素提前拿出来并缓存起来 + if iter.hasNext() { + this.nextElem = iter.next() + } + return this +} + +// 返回迭代器的下一个元素,但是不移动迭代器的指针 +func (this *PeekingIterator) peek() int { + return this.nextElem +} + +//以下方法和 Iterator 接口中的一样。 +func (this *PeekingIterator) next() int { + res := this.nextElem + // 如果迭代器还有下一个元素,就更新缓存 + if this.iter.hasNext() { + this.nextElem = this.iter.next() + } else { + this.nextElem = 0 + } + return res +} + +func (this *PeekingIterator) hasNext() bool { + return this.nextElem != 0 +} +``` + +```java +// by labuladong (java) +// Java Iterator interface reference: +// https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html + +class PeekingIterator implements Iterator { + private Iterator iter; + // 把迭代器的下一个元素提前拿出来并缓存起来 + private Integer nextElem; + + public PeekingIterator(Iterator iterator) { + this.iter = iterator; + this.nextElem = iterator.next(); + } + + public Integer peek() { + return nextElem; + } + + + @Override + public Integer next() { + Integer res = nextElem; + // 更新 nextElem + if (iter.hasNext()) { + nextElem = iter.next(); + } else { + nextElem = null; + } + return res; + } + + @Override + public boolean hasNext() { + return nextElem != null; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * Javascript iterator reference: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol + */ + +var PeekingIterator = function(iterator) { + this.iter = iterator; + // 把迭代器的下一个元素提前拿出来并缓存起来 + this.nextElem = iterator.next(); +}; + +PeekingIterator.prototype.peek = function() { + return this.nextElem; +}; + +PeekingIterator.prototype.next = function() { + var res = this.nextElem; + // 更新 nextElem + if (this.iter.hasNext()) { + this.nextElem = this.iter.next(); + } else { + this.nextElem = null; + } + return res; +}; + +PeekingIterator.prototype.hasNext = function() { + return this.nextElem !== null; +}; +``` + +```python +# by chatGPT (python) +# Python Iterator interface reference: +# https://docs.python.org/3/library/stdtypes.html#iterator-types + +class PeekingIterator: + def __init__(self, iterator): + self.iter = iterator + # 把迭代器的下一个元素提前拿出来并缓存起来 + self.next_elem = self.iter.__next__() + + def peek(self): + return self.next_elem + + def __next__(self): + res = self.next_elem + # 更新 next_elem + try: + self.next_elem = self.iter.__next__() + except StopIteration: + self.next_elem = None + return res + + def __iter__(self): + return self + + def __bool__(self): + return self.next_elem is not None +``` + +https://leetcode.cn/problems/peeking-iterator 的多语言解法👆 + +https://leetcode.cn/problems/perfect-rectangle 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isRectangleCover(vector>& rectangles) { + int X1 = INT_MAX, Y1 = INT_MAX; + int X2 = INT_MIN, Y2 = INT_MIN; + set> points; + int actual_area = 0; + for(auto &r: rectangles){ + //计算完美矩形的理论顶点坐标 + X1 = min(X1, r[0]); + Y1 = min(Y1, r[1]); + X2 = max(X2, r[2]); + Y2 = max(Y2, r[3]); + //累加小矩形的面积 + actual_area += (r[2] - r[0]) * (r[3] - r[1]); + + //记录最终形成的图形中的顶点 + pair p1 = {r[0], r[1]}; + pair p2 = {r[0], r[3]}; + pair p3 = {r[2], r[1]}; + pair p4 = {r[2], r[3]}; + vector> cur_points = {p1, p2, p3, p4}; + //如果坐标已经存在,那么从set里面删除 + //如果坐标不存在,那么添加 + for(auto &p: cur_points){ + if(points.count(p)) points.erase(p); + else points.insert(p); + } + } + // 判断面积是否相同 + int expected_area = (X2 - X1) * (Y2 - Y1); + if(actual_area != expected_area) return false; + // 判断最终留下的顶点个数是否为 4 + if(points.size() != 4) return false; + // 判断留下的 4 个顶点是否是完美矩形的顶点 + if(!points.count({X1, Y1})) return false; + if(!points.count({X1, Y2})) return false; + if(!points.count({X2, Y1})) return false; + if(!points.count({X2, Y2})) return false; + // 面积和顶点都对应,说明矩形符合题意 + return true; + } +}; +``` + +```go +// by chatGPT (go) +func isRectangleCover(rectangles [][]int) bool { + inf := 1<<31 - 1 + X1, Y1 := inf, inf + X2, Y2 := -inf, -inf + + points := make(map[[2]int]bool) + actual_area := 0 + for _, rectangle := range rectangles { + // 计算完美矩形的理论顶点坐标 + x1, y1, x2, y2 := rectangle[0], rectangle[1], rectangle[2], rectangle[3] + X1, Y1 = min(X1, x1), min(Y1, y1) + X2, Y2 = max(X2, x2), max(Y2, y2) + // 累加小矩形的面积 + actual_area += (x2 - x1) * (y2 - y1) + // 记录最终形成的图形中的顶点 + p1, p2 := [2]int{x1, y1}, [2]int{x1, y2} + p3, p4 := [2]int{x2, y1}, [2]int{x2, y2} + for _, p := range [][2]int{p1, p2, p3, p4} { + if _, ok := points[p]; ok { + delete(points, p) + } else { + points[p] = true + } + } + } + // 判断面积是否相同 + expected_area := (X2 - X1) * (Y2 - Y1) + if actual_area != expected_area { + return false + } + // 判断最终留下的顶点个数是否为 4 + if len(points) != 4 { + return false + } + // 判断留下的 4 个顶点是否是完美矩形的顶点 + if !points[[2]int{X1, Y1}] { + return false + } + if !points[[2]int{X1, Y2}] { + return false + } + if !points[[2]int{X2, Y1}] { + return false + } + if !points[[2]int{X2, Y2}] { + return false + } + // 面积和顶点都对应,说明矩形符合题意 + return true +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by chatGPT (java) +class Solution { + public boolean isRectangleCover(int[][] rectangles) { + int X1 = Integer.MAX_VALUE, Y1 = Integer.MAX_VALUE; + int X2 = Integer.MIN_VALUE, Y2 = Integer.MIN_VALUE; + + Set points = new HashSet<>(); + int actual_area = 0; + + for(int[] rectangle: rectangles){ + // 计算完美矩形的理论顶点坐标 + X1 = Math.min(X1, rectangle[0]); + Y1 = Math.min(Y1, rectangle[1]); + X2 = Math.max(X2, rectangle[2]); + Y2 = Math.max(Y2, rectangle[3]); + // 累加小矩形的面积 + actual_area += (rectangle[2] - rectangle[0]) * (rectangle[3] - rectangle[1]); + // 记录最终形成的图形中的顶点 + String p1 = rectangle[0] + "," + rectangle[1]; + String p2 = rectangle[0] + "," + rectangle[3]; + String p3 = rectangle[2] + "," + rectangle[1]; + String p4 = rectangle[2] + "," + rectangle[3]; + for (String p : new String[] { p1, p2, p3, p4 }) { + if (points.contains(p)) + points.remove(p); + else + points.add(p); + } + } + // 判断面积是否相同 + int expected_area = (X2 - X1) * (Y2 - Y1); + if(actual_area!=expected_area){ + return false; + } + // 判断最终留下的顶点个数是否为 4 + if(points.size()!=4){ + return false; + } + // 判断留下的 4 个顶点是否是完美矩形的顶点 + if (!points.contains(X1 + "," + Y1)) + return false; + if (!points.contains(X1 + "," + Y2)) + return false; + if (!points.contains(X2 + "," + Y1)) + return false; + if (!points.contains(X2 + "," + Y2)) + return false; + // 面积和顶点都对应,说明矩形符合题意 + return true; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** +* @param {number[][]} rectangles +* @return {boolean} +*/ +var isRectangleCover = function(rectangles) { + let X1 = Infinity, Y1 = Infinity; // 记录最左下角点的坐标 + let X2 = -Infinity, Y2 = -Infinity; // 记录最右上角点的坐标 + let points = new Set(); // 记录每个小矩形的四个角落 + let actual_area = 0; // 记录小矩形的面积之和 + for(let i=0; i bool: + X1, Y1 = float('inf'), float('inf') + X2, Y2 = -float('inf'), -float('inf') + + points = set() + actual_area = 0 + for x1, y1, x2, y2 in rectangles: + # 计算完美矩形的理论顶点坐标 + X1, Y1 = min(X1, x1), min(Y1, y1) + X2, Y2 = max(X2, x2), max(Y2, y2) + # 累加小矩形的面积 + actual_area += (x2 - x1) * (y2 - y1) + # 记录最终形成的图形中的顶点 + p1, p2 = (x1, y1), (x1, y2) + p3, p4 = (x2, y1), (x2, y2) + for p in [p1, p2, p3, p4]: + if p in points: points.remove(p) + else: points.add(p) + # 判断面积是否相同 + expected_area = (X2 - X1) * (Y2 - Y1) + if actual_area != expected_area: + return False + # 判断最终留下的顶点个数是否为 4 + if len(points) != 4: return False + # 判断留下的 4 个顶点是否是完美矩形的顶点 + if (X1, Y1) not in points: return False + if (X1, Y2) not in points: return False + if (X2, Y1) not in points: return False + if (X2, Y2) not in points: return False + # 面积和顶点都对应,说明矩形符合题意 + return True +``` + +https://leetcode.cn/problems/perfect-rectangle 的多语言解法👆 + +https://leetcode.cn/problems/perfect-squares 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int numSquares(int n) { + // 定义:和为 i 的平方数的最小数量是 dp[i] + vector dp(n + 1); + // base case + dp[0] = 0; + // 状态转移方程 + for (int i = 1; i <= n; i++) { + dp[i] = i; // 最坏的情况就是每个数都是 1,最多也就 i 个数 + for (int j = 1; j * j <= i; j++) { + // i - j * j 只要再加一个平方数 j * j 即可凑出 i + dp[i] = min(dp[i], dp[i - j * j] + 1); + } + } + return dp[n]; + } +}; +``` + +```go +// by chatGPT (go) +func numSquares(n int) int { + // 定义:和为 i 的平方数的最小数量是 dp[i] + dp := make([]int, n+1) + // base case + dp[0] = 0 + // 状态转移方程 + for i := 1; i <= n; i++ { + dp[i] = i + for j := 1; j*j <= i; j++ { + dp[i] = min(dp[i], dp[i-j*j]+1) + } + } + return dp[n] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int numSquares(int n) { + // 定义:和为 i 的平方数的最小数量是 dp[i] + int[] dp = new int[n + 1]; + Arrays.fill(dp, Integer.MAX_VALUE); + // base case + dp[0] = 0; + // 状态转移方程 + for (int i = 1; i <= n; i++) { + for (int j = 1; j * j <= i; j++) { + // i - j * j 只要再加一个平方数 j * j 即可凑出 i + dp[i] = Math.min(dp[i], dp[i - j * j] + 1); + } + } + return dp[n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var numSquares = function(n) { + // 定义:和为 i 的平方数的最小数量是 dp[i] + const dp = new Array(n+1).fill(0); + // base case + dp[0] = 0; + // 状态转移方程 + for (let i = 1; i <= n; i++) { + dp[i] = i; + for (let j = 1; j * j <= i; j++) { + // i - j * j 只要再加一个平方数 j * j 即可凑出 i + dp[i] = Math.min(dp[i], dp[i - j * j] + 1); + } + } + return dp[n]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def numSquares(self, n: int) -> int: + # 定义:和为 i 的平方数的最小数量是 dp[i] + dp = [float('inf')] * (n+1) + # base case + dp[0] = 0 + # 状态转移方程 + for i in range(1, n+1): + for j in range(1, int(i**0.5)+1): + # i - j * j 只要再加一个平方数 j * j 即可凑出 i + dp[i] = min(dp[i], dp[i - j*j] + 1) + return dp[n] +``` + +https://leetcode.cn/problems/perfect-squares 的多语言解法👆 + +https://leetcode.cn/problems/permutation-in-string 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + + // 判断 s 中是否存在 t 的排列 + bool checkInclusion(string t, string s) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + while (right < s.size()) { + char c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + if (need.count(c)) { + window[c]++; + if (window[c] == need[c]) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (right - left >= t.size()) { + // 在这里判断是否找到了合法的子串 + if (valid == need.size()) + return true; + char d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + if (need.count(d)) { + if (window[d] == need[d]) + valid--; + window[d]--; + } + } + } + // 未找到符合条件的子串 + return false; + } +}; +``` + +```go +// by chatGPT (go) +// 判断 s 中是否存在 t 的排列 +func checkInclusion(t string, s string) bool { + need, window := make(map[byte]int), make(map[byte]int) + for i := range t { + need[t[i]]++ + } + + left, right := 0, 0 + valid := 0 + for right < len(s) { + c := s[right] + right++ + // 进行窗口内数据的一系列更新 + if _, ok := need[c]; ok { + window[c]++ + if window[c] == need[c] { + valid++ + } + } + + // 判断左侧窗口是否要收缩 + for right-left >= len(t) { + // 在这里判断是否找到了符合条件的子串 + if valid == len(need) { + return true + } + d := s[left] + left++ + // 进行窗口内数据的一系列更新 + if _, ok := need[d]; ok { + if window[d] == need[d] { + valid-- + } + window[d]-- + } + } + } + // 未找到符合条件的子串 + return false +} +``` + +```java +// by chatGPT (java) +class Solution { + public boolean checkInclusion(String t, String s) { + //使用哈希表用于记录 t 中每个字符出现的次数 + Map need = new HashMap<>(), window = new HashMap<>(); + for (int i = 0; i < t.length(); i++) { + char c = t.charAt(i); + need.put(c, need.getOrDefault(c, 0) + 1); + } + + //初始化窗口的左右边界及合法字符个数 + int left = 0, right = 0, valid = 0; + while (right < s.length()) { + //c 是将移入窗口的字符 + char c = s.charAt(right); + //右移窗口 + right++; + //进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) + valid++; + } + + //判断左侧窗口是否要收缩 + while (right - left >= t.length()) { + //在这里判断是否找到了合法的子串 + if (valid == need.size()) + return true; + //d 是将移出窗口的字符 + char d = s.charAt(left); + //左移窗口 + left++; + + //进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) + valid--; + window.put(d, window.get(d) - 1); + } + } + } + //未找到符合条件的子串 + return false; + } +} +``` + +```javascript +// by chatGPT (javascript) +var checkInclusion = function(t, s) { + // 初始化 need 和 window 哈希表 + let need = new Map(); + let window = new Map(); + for (let c of t) { + need.set(c, (need.get(c) || 0) + 1); + } + + let left = 0; + let right = 0; + let valid = 0; + while (right < s.length) { + let c = s[right]; + right++; + // 进行窗口内数据的更新 + if (need.has(c)) { + window.set(c, (window.get(c) || 0) + 1); + if (window.get(c) === need.get(c)) { + valid++; + } + } + + // 判断左侧窗口是否需要收缩 + while (right - left >= t.length) { + // 更新结果 + if (valid === need.size) { + return true; + } + let d = s[left]; + left++; + // 进行窗口内数据的更新 + if (need.has(d)) { + if (window.get(d) === need.get(d)) { + valid--; + } + window.set(d, (window.get(d) || 0) - 1); + } + } + } + // 未找到符合条件的子串 + return false; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def checkInclusion(self, t: str, s: str) -> bool: + # 创建字典,记录字符需要出现的次数 + need, window = Counter(t), Counter() + left, right, valid = 0, 0, 0 + + # 右指针前移,更新窗口内数据 + while right < len(s): + c = s[right] + right += 1 + # 如果该字符在需要的字典里,更新窗口内字典 + if need[c]: + window[c] += 1 + # 如果窗口内字典该字符准确次数与需要的次数相同,计数器+1 + if window[c] == need[c]: + valid += 1 + + # 判断左侧窗口是否需要收缩 + while right - left >= len(t): + # 如果子串合法,返回True + if valid == len(need): + return True + # 左指针前移了,需要从窗口内字典中减掉一个元素 + d = s[left] + left += 1 + if need[d]: + # 如果窗口内字典该字符准确次数与需要的次数相同,计数器-1 + if window[d] == need[d]: + valid -= 1 + window[d] -= 1 + # 未找到合法的子串,返回False + return False +``` + +https://leetcode.cn/problems/permutation-in-string 的多语言解法👆 + +https://leetcode.cn/problems/permutations 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> res; + + /* 主函数,输入一组不重复的数字,返回它们的全排列 */ + vector> permute(vector& nums) { + // 记录「路径」 + deque track; + // 「路径」中的元素会被标记为 true,避免重复使用 + vector used(nums.size(), false); + + backtrack(nums, track, used); + return res; + } + + // 路径:记录在 track 中 + // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + // 结束条件:nums 中的元素全都在 track 中出现 + void backtrack(vector& nums, deque& track, vector& used) { + // 触发结束条件 + if (track.size() == nums.size()) { + res.push_back(vector(track.begin(), track.end())); + return; + } + + for (int i = 0; i < nums.size(); i++) { + // 排除不合法的选择 + if (used[i]) { + /** + ![](../pictures/backtracking/6.jpg) + */ + // nums[i] 已经在 track 中,跳过 + continue; + } + // 做选择 + track.push_back(nums[i]); + used[i] = true; + // 进入下一层决策树 + backtrack(nums, track, used); + // 取消选择 + track.pop_back(); + used[i] = false; + } + } +}; +``` + +```go +// by chatGPT (go) +func permute(nums []int) [][]int { + res := [][]int{} + track := []int{} // 记录「路径」 + used := make([]bool, len(nums)) // 「路径」中的元素会被标记为 true,避免重复使用 + + var backtrack func(int) // 定义回溯函数 + + backtrack = func(level int) { + if level == len(nums) { // 触发结束条件 + tmp := make([]int, len(track)) + copy(tmp, track) + res = append(res, tmp) + return + } + // 枚举出所有可能的选择 + for i := 0; i < len(nums); i++ { + if used[i] { // 排除不合法的选择 + continue + } + track = append(track, nums[i]) // 做选择 + used[i] = true + backtrack(level+1) // 进入下一层决策树 + track = track[:len(track)-1] // 取消选择 + used[i] = false + } + } + + backtrack(0) + return res +} +``` + +```java +// by labuladong (java) +class Solution { + + List> res = new LinkedList<>(); + + /* 主函数,输入一组不重复的数字,返回它们的全排列 */ + List> permute(int[] nums) { + // 记录「路径」 + LinkedList track = new LinkedList<>(); + // 「路径」中的元素会被标记为 true,避免重复使用 + boolean[] used = new boolean[nums.length]; + + backtrack(nums, track, used); + return res; + } + + // 路径:记录在 track 中 + // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + // 结束条件:nums 中的元素全都在 track 中出现 + void backtrack(int[] nums, LinkedList track, boolean[] used) { + // 触发结束条件 + if (track.size() == nums.length) { + res.add(new LinkedList(track)); + return; + } + + for (int i = 0; i < nums.length; i++) { + // 排除不合法的选择 + if (used[i]) { + /** + ![](../pictures/backtracking/6.jpg) + */ + // nums[i] 已经在 track 中,跳过 + continue; + } + // 做选择 + track.add(nums[i]); + used[i] = true; + // 进入下一层决策树 + backtrack(nums, track, used); + // 取消选择 + track.removeLast(); + used[i] = false; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var permute = function(nums) { + let res = []; + let track = []; + let used = new Array(nums.length).fill(false); + // 路径:记录在 track 中 + // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + // 结束条件:nums 中的元素全都在 track 中出现 + const backtrack = (nums, track, used) => { + // 触发结束条件 + if (track.length === nums.length) { + res.push([...track]); + return; + } + + for (let i = 0; i < nums.length; i++) { + // 排除不合法的选择 + if (used[i]) { + // nums[i] 已经在 track 中,跳过 + continue; + } + // 做选择 + track.push(nums[i]); + used[i] = true; + // 进入下一层决策树 + backtrack(nums, track, used); + // 取消选择 + track.pop(); + used[i] = false; + } + } + backtrack(nums, track, used); + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + + # 主函数,输入一组不重复的数字,返回它们的全排列 + def permute(self, nums: List[int]) -> List[List[int]]: + + # 记录「路径」 + track = [] + # 「路径」中的元素会被标记为 true,避免重复使用 + used = [False] * len(nums) + + self.backtrack(nums, track, used) + return self.res + + # 路径:记录在 track 中 + # 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + # 结束条件:nums 中的元素全都在 track 中出现 + def backtrack(self, nums: List[int], track: List[int], used: List[bool]) -> None: + + # 触发结束条件 + if len(track) == len(nums): + self.res.append(track[:]) + return + + for i in range(len(nums)): + # 排除不合法的选择 + if used[i]: + # nums[i] 已经在 track 中,跳过 + continue + # 做选择 + track.append(nums[i]) + used[i] = True + # 进入下一层决策树 + self.backtrack(nums, track, used) + # 取消选择 + track.pop() + used[i] = False +``` + +https://leetcode.cn/problems/permutations 的多语言解法👆 + +https://leetcode.cn/problems/populating-next-right-pointers-in-each-node 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 主函数 + Node* connect(Node* root) { + if (root == nullptr) return nullptr; + // 遍历「三叉树」,连接相邻节点 + traverse(root->left, root->right); + return root; + } + + // 三叉树遍历框架 + void traverse(Node* node1, Node* node2) { + if (node1 == nullptr || node2 == nullptr) { + return; + } + /**** 前序位置 ****/ + // 将传入的两个节点穿起来 + node1->next = node2; + + // 连接相同父节点的两个子节点 + traverse(node1->left, node1->right); + traverse(node2->left, node2->right); + // 连接跨越父节点的两个子节点 + traverse(node1->right, node2->left); + } +}; +``` + +```go +// by chatGPT (go) +func connect(root *Node) *Node { + if root == nil { + return nil + } + // 二叉树层序遍历框架 + q := make([]*Node, 0) + q = append(q, root) + for len(q) > 0 { + sz := len(q) + // 遍历一层 + var pre *Node + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + // 链接当前层所有节点的 next 指针 + if pre != nil { + pre.Next = cur + } + pre = cur + // 将下一层节点装入队列 + if cur.Left != nil { + q = append(q, cur.Left) + } + if cur.Right != nil { + q = append(q, cur.Right) + } + } + } + return root +} +``` + +```java +// by labuladong (java) +class Solution { + // 主函数 + public Node connect(Node root) { + if (root == null) return null; + // 遍历「三叉树」,连接相邻节点 + traverse(root.left, root.right); + return root; + } + + // 三叉树遍历框架 + void traverse(Node node1, Node node2) { + if (node1 == null || node2 == null) { + return; + } + /**** 前序位置 ****/ + // 将传入的两个节点穿起来 + node1.next = node2; + + // 连接相同父节点的两个子节点 + traverse(node1.left, node1.right); + traverse(node2.left, node2.right); + // 连接跨越父节点的两个子节点 + traverse(node1.right, node2.left); + } +} +``` + +```javascript +// by chatGPT (javascript) +var connect = function(root) { + if (root === null) { + return null; + } + // 二叉树层序遍历框架 + const q = []; + q.push(root); + while (q.length > 0) { + const sz = q.length; + // 遍历一层 + let pre = null; + for (let i = 0; i < sz; i++) { + const cur = q.shift(); + // 链接当前层所有节点的 next 指针 + if (pre !== null) { + pre.next = cur; + } + pre = cur; + // 将下一层节点装入队列 + if (cur.left !== null) { + q.push(cur.left); + } + if (cur.right !== null) { + q.push(cur.right); + } + } + } + return root; +}; +``` + +```python +# by chatGPT (python) +# Definition for a Node. +# class Node: +# def __init__(self, val = 0, left = None, right = None, next = None): +# self.val = val +# self.left = left +# self.right = right +# self.next = next + +class Solution: + # 主函数 + def connect(self, root: 'Node') -> 'Node': + if not root: + return + + # 遍历「三叉树」,连接相邻节点 + self.traverse(root.left, root.right) + + return root + + # 三叉树遍历框架 + def traverse(self, node1, node2): + # 终止条件 + if not node1 or not node2: + return + # 将传入的两个节点穿起来 + node1.next = node2 + # 连接相同父节点的两个子节点 + self.traverse(node1.left, node1.right) + self.traverse(node2.left, node2.right) + # 连接跨越父节点的两个子节点 + self.traverse(node1.right, node2.left) +``` + +https://leetcode.cn/problems/populating-next-right-pointers-in-each-node 的多语言解法👆 + +https://leetcode.cn/problems/possible-bipartition 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + bool ok = true; + vector color; + vector visited; + +public: + bool possibleBipartition(int n, vector>& dislikes) { + // 图节点编号从 1 开始 + color.resize(n + 1); + visited.resize(n + 1); + // 转化成邻接表表示图结构 + vector> graph = buildGraph(n, dislikes); + + for (int v = 1; v <= n; v++) { + if (!visited[v]) { + traverse(graph, v); + } + } + return ok; + } + + // 建图函数 + vector> buildGraph(int n, vector>& dislikes) { + // 图节点编号为 1...n + vector> graph(n + 1); + for (int i = 1; i <= n; i++) { + graph[i] = vector(); + } + for (auto& edge : dislikes) { + int v = edge[1]; + int w = edge[0]; + // 「无向图」相当于「双向图」 + // v -> w + graph[v].push_back(w); + // w -> v + graph[w].push_back(v); + } + return graph; + } + + // 和之前判定二分图的 traverse 函数完全相同 + void traverse(vector>& graph, int v) { + if (!ok) return; + visited[v] = true; + for (int w : graph[v]) { + if (!visited[w]) { + color[w] = !color[v]; + traverse(graph, w); + } else { + if (color[w] == color[v]) { + ok = false; + } + } + } + } +}; +``` + +```go +// by chatGPT (go) +import "fmt" + +func possibleBipartition(n int, dislikes [][]int) bool { + color := make([]bool, n+1) + visited := make([]bool, n+1) + ok := true + + // 转化成邻接表表示图结构 + graph := buildGraph(n, dislikes) + + var traverse func(int) + traverse = func(v int) { + if !ok { + return + } + visited[v] = true + for _, w := range graph[v] { + if !visited[w] { + color[w] = !color[v] + traverse(w) + } else { + if color[w] == color[v] { + ok = false + } + } + } + } + + // 全图遍历,防止非连通图出现 + for v := 1; v <= n; v++ { + if !visited[v] { + traverse(v) + } + } + + return ok +} + +// 建图函数 +func buildGraph(n int, dislikes [][]int) []([]int) { + graph := make([]([]int), n+1) + for i := range graph { + graph[i] = make([]int, 0) + } + for _, edge := range dislikes { + v := edge[1] + w := edge[0] + // 「无向图」相当于「双向图」 + // v -> w + graph[v] = append(graph[v], w) + // w -> v + graph[w] = append(graph[w], v) + } + return graph +} +``` + +```java +// by labuladong (java) +class Solution { + + private boolean ok = true; + private boolean[] color; + private boolean[] visited; + + public boolean possibleBipartition(int n, int[][] dislikes) { + // 图节点编号从 1 开始 + color = new boolean[n + 1]; + visited = new boolean[n + 1]; + // 转化成邻接表表示图结构 + List[] graph = buildGraph(n, dislikes); + + for (int v = 1; v <= n; v++) { + if (!visited[v]) { + traverse(graph, v); + } + } + return ok; + } + + // 建图函数 + private List[] buildGraph(int n, int[][] dislikes) { + // 图节点编号为 1...n + List[] graph = new LinkedList[n + 1]; + for (int i = 1; i <= n; i++) { + graph[i] = new LinkedList<>(); + } + for (int[] edge : dislikes) { + int v = edge[1]; + int w = edge[0]; + // 「无向图」相当于「双向图」 + // v -> w + graph[v].add(w); + // w -> v + graph[w].add(v); + } + return graph; + } + + // 和之前判定二分图的 traverse 函数完全相同 + private void traverse(List[] graph, int v) { + if (!ok) return; + visited[v] = true; + for (int w : graph[v]) { + if (!visited[w]) { + color[w] = !color[v]; + traverse(graph, w); + } else { + if (color[w] == color[v]) { + ok = false; + } + } + } + } + +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @param {number[][]} dislikes + * @return {boolean} + */ +var possibleBipartition = function(n, dislikes) { + let ok = true; + let color = new Array(n + 1); + let visited = new Array(n + 1); + let graph = buildGraph(n, dislikes); + // 建图函数 + function buildGraph(n, dislikes) { + // 图节点编号为 1...n + let graph = new Array(n + 1); + for (let i = 1; i <= n; i++) { + graph[i] = new Array(); + } + for (let i = 0; i < dislikes.length; i++) { + let v = dislikes[i][0]; + let w = dislikes[i][1]; + // 「无向图」相当于「双向图」 + // v -> w + graph[v].push(w); + // w -> v + graph[w].push(v); + } + return graph; + } + // 和之前判定二分图的 traverse 函数完全相同 + function traverse(graph, v) { + if (!ok) return; + visited[v] = true; + for (let i = 0; i < graph[v].length; i++) { + let w = graph[v][i]; + if (!visited[w]) { + color[w] = !color[v]; + traverse(graph, w); + } else { + if (color[w] == color[v]) { + ok = false; + } + } + } + } + for (let v = 1; v <= n; v++) { + if (!visited[v]) { + traverse(graph, v); + } + } + return ok; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.ok = True + self.color = None + self.visited = None + + def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool: + # 图节点编号从 1 开始 + self.color = [False] * (n + 1) + self.visited = [False] * (n + 1) + # 转化成邻接表表示图结构 + graph = self.buildGraph(n, dislikes) + + for v in range(1, n+1): + if not self.visited[v]: + self.traverse(graph, v) + return self.ok + + # 建图函数 + def buildGraph(self, n: int, dislikes: List[List[int]]) -> List[List[int]]: + # 图节点编号为 1...n + graph = [[] for _ in range(n + 1)] + for edge in dislikes: + v = edge[1] + w = edge[0] + # 「无向图」相当于「双向图」 + # v -> w + graph[v].append(w) + # w -> v + graph[w].append(v) + return graph + + # 和之前判定二分图的 traverse 函数完全相同 + def traverse(self, graph: List[List[int]], v: int) -> None: + if not self.ok: + return + self.visited[v] = True + for w in graph[v]: + if not self.visited[w]: + self.color[w] = not self.color[v] + self.traverse(graph, w) + else: + if self.color[w] == self.color[v]: + self.ok = False +``` + +https://leetcode.cn/problems/possible-bipartition 的多语言解法👆 + +https://leetcode.cn/problems/power-of-two 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isPowerOfTwo(int n) { + if (n <= 0) return false; + return (n & (n - 1)) == 0; + } +}; +``` + +```go +// by chatGPT (go) +// Solution to LeetCode problem: Power of Two +// https://leetcode.com/problems/power-of-two/ +import "fmt" + +func isPowerOfTwo(n int) bool { + if n <= 0 { + return false + } + return (n & (n - 1)) == 0 +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isPowerOfTwo(int n) { + if (n <= 0) return false; + return (n & (n - 1)) == 0; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {boolean} + */ +var isPowerOfTwo = function(n) { + if (n <= 0) return false; + return (n & (n - 1)) == 0; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isPowerOfTwo(self, n: int) -> bool: + if n <= 0: + return False + return (n & (n - 1)) == 0 +``` + +https://leetcode.cn/problems/power-of-two 的多语言解法👆 + +https://leetcode.cn/problems/powx-n 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + double myPow(double a, int k) { + if (k == 0) return 1; + + if (k == INT_MIN) { + // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a; + } + + if (k < 0) { + return myPow(1 / a, -k); + } + + if (k % 2 == 1) { + // k 是奇数 + return (a * myPow(a, k - 1)); + } else { + // k 是偶数 + double sub = myPow(a, k / 2); + return (sub * sub); + } + } +}; +``` + +```go +// by chatGPT (go) +// Golang Code +func myPow(a float64, k int) float64 { + if k == 0 { + return 1 + } + + if k == math.MinInt32 { + // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a + } + + if k < 0 { + return myPow(1 / a, -k) + } + if k % 2 == 1 { + // k 是奇数 + return (a * myPow(a, k - 1)) + } else { + // k 是偶数 + sub := myPow(a, k / 2) + return (sub * sub) + } +} +``` + +```java +// by labuladong (java) +class Solution { + public double myPow(double a, int k) { + if (k == 0) return 1; + + if (k == Integer.MIN_VALUE) { + // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a; + } + + if (k < 0) { + return myPow(1 / a, -k); + } + + if (k % 2 == 1) { + // k 是奇数 + return (a * myPow(a, k - 1)); + } else { + // k 是偶数 + double sub = myPow(a, k / 2); + return (sub * sub); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} a + * @param {number} k + * @return {number} + */ +var myPow = function(a, k) { + if (k === 0) return 1; + + if (k === -2147483648) { + // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a; + } + + if (k < 0) { + return myPow(1 / a, -k); + } + + if (k % 2 === 1) { + // k 是奇数 + return (a * myPow(a, k - 1)); + } else { + // k 是偶数 + const sub = myPow(a, k / 2); + return (sub * sub); + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def myPow(self, a: float, k: int) -> float: + if k == 0: + return 1 + + if k == -2147483648: + # 把 k 是 INT_MIN 的情况单独拿出来处理 + # 避免 -k 整型溢出 + return self.myPow(1 / a, -(k + 1)) / a + + if k < 0: + return self.myPow(1 / a, -k) + + if k % 2 == 1: + # k 是奇数 + return a * self.myPow(a, k - 1) + else: + # k 是偶数 + sub = self.myPow(a, k // 2) + return sub * sub +``` + +https://leetcode.cn/problems/powx-n 的多语言解法👆 + +https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int preimageSizeFZF(int K) { + // 左边界和右边界之差 + 1 就是答案 + return (int)(right_bound(K) - left_bound(K) + 1); + } + + // 逻辑不变,数据类型全部改成 long + long trailingZeroes(long n) { + long res = 0; + for (long d = n; d / 5 > 0; d = d / 5) { + res += d / 5; + } + return res; + } + + /* 搜索 trailingZeroes(n) == K 的左侧边界 */ + long left_bound(int target) { + long lo = 0, hi = LONG_MAX; + while (lo < hi) { + long mid = lo + (hi - lo) / 2; + if (trailingZeroes(mid) < target) { + lo = mid + 1; + } else if (trailingZeroes(mid) > target) { + hi = mid; + } else { + hi = mid; + } + } + return lo; + } + + /* 搜索 trailingZeroes(n) == K 的右侧边界 */ + long right_bound(int target) { + long lo = 0, hi = LONG_MAX; + while (lo < hi) { + long mid = lo + (hi - lo) / 2; + if (trailingZeroes(mid) < target) { + lo = mid + 1; + } else if (trailingZeroes(mid) > target) { + hi = mid; + } else { + lo = mid + 1; + } + } + + return lo - 1; + } +}; +``` + +```go +// by chatGPT (go) +import "math" + +func preimageSizeFZF(K int) int { + // 左边界和右边界之差 + 1 就是答案 + return int(right_bound(K) - left_bound(K) + 1) +} + +// 逻辑不变,数据类型全部改成 int64 +func trailingZeroes(n int64) int64 { + var res int64 = 0 + for d := n; d / 5 > 0; d = d / 5 { + res += d / 5 + } + return res +} + +/* 搜索 trailingZeroes(n) == K 的左侧边界 */ +func left_bound(target int) int64 { + var lo, hi int64 = 0, math.MaxInt64 + for lo < hi { + mid := lo + (hi - lo) / 2 + if trailingZeroes(mid) < int64(target) { + lo = mid + 1 + } else if trailingZeroes(mid) > int64(target) { + hi = mid + } else { + hi = mid + } + } + return lo +} + +/* 搜索 trailingZeroes(n) == K 的右侧边界 */ +func right_bound(target int) int64 { + var lo, hi int64 = 0, math.MaxInt64 + for lo < hi { + mid := lo + (hi - lo) / 2 + if trailingZeroes(mid) < int64(target) { + lo = mid + 1 + } else if trailingZeroes(mid) > int64(target) { + hi = mid + } else { + lo = mid + 1 + } + } + + return lo - 1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int preimageSizeFZF(int K) { + // 左边界和右边界之差 + 1 就是答案 + return (int)(right_bound(K) - left_bound(K) + 1); + } + + // 逻辑不变,数据类型全部改成 long + long trailingZeroes(long n) { + long res = 0; + for (long d = n; d / 5 > 0; d = d / 5) { + res += d / 5; + } + return res; + } + + /* 搜索 trailingZeroes(n) == K 的左侧边界 */ + long left_bound(int target) { + long lo = 0, hi = Long.MAX_VALUE; + while (lo < hi) { + long mid = lo + (hi - lo) / 2; + if (trailingZeroes(mid) < target) { + lo = mid + 1; + } else if (trailingZeroes(mid) > target) { + hi = mid; + } else { + hi = mid; + } + } + return lo; + } + + /* 搜索 trailingZeroes(n) == K 的右侧边界 */ + long right_bound(int target) { + long lo = 0, hi = Long.MAX_VALUE; + while (lo < hi) { + long mid = lo + (hi - lo) / 2; + if (trailingZeroes(mid) < target) { + lo = mid + 1; + } else if (trailingZeroes(mid) > target) { + hi = mid; + } else { + lo = mid + 1; + } + } + + return lo - 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var preimageSizeFZF = function (K) { + // 左边界和右边界之差 + 1 就是答案 + return right_bound(K) - left_bound(K) + 1; +}; + +// 逻辑不变,数据类型全部改成 long +var trailingZeroes = function (n) { + var res = 0; + for (var d = n; Math.floor(d / 5) > 0; d = Math.floor(d / 5)) { + res += Math.floor(d / 5); + } + return res; +}; + +/* 搜索 trailingZeroes(n) == K 的左侧边界 */ +var left_bound = function (target) { + var lo = 0, + hi = Number.MAX_VALUE; + while (lo < hi) { + var mid = lo + Math.floor((hi - lo) / 2); + if (trailingZeroes(mid) < target) { + lo = mid + 1; + } else if (trailingZeroes(mid) > target) { + hi = mid; + } else { + hi = mid; + } + } + return lo; +}; + +/* 搜索 trailingZeroes(n) == K 的右侧边界 */ +var right_bound = function (target) { + var lo = 0, + hi = Number.MAX_VALUE; + while (lo < hi) { + var mid = lo + Math.floor((hi - lo) / 2); + if (trailingZeroes(mid) < target) { + lo = mid + 1; + } else if (trailingZeroes(mid) > target) { + hi = mid; + } else { + lo = mid + 1; + } + } + + return lo - 1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def preimageSizeFZF(self, K: int) -> int: + # 左边界和右边界之差 + 1 就是答案 + return int(self.right_bound(K) - self.left_bound(K) + 1) + + # 逻辑不变,数据类型全部改成 long + def trailingZeroes(self, n: int) -> int: + res = 0 + for d in range(n, 0, -1): + while d % 5 == 0: + res += 1 + d //= 5 + return res + + """ 搜索 trailingZeroes(n) == K 的左侧边界 """ + def left_bound(self, target: int) -> int: + lo, hi = 0, float('inf') + while lo < hi: + mid = lo + (hi - lo) // 2 + if self.trailingZeroes(mid) < target: + lo = mid + 1 + elif self.trailingZeroes(mid) > target: + hi = mid + else: + hi = mid + + return lo + + + """ 搜索 trailingZeroes(n) == K 的右侧边界 """ + def right_bound(self, target: int) -> int: + lo, hi = 0, float('inf') + while lo < hi: + mid = lo + (hi - lo) // 2 + if self.trailingZeroes(mid) < target: + lo = mid + 1 + elif self.trailingZeroes(mid) > target: + hi = mid + else: + lo = mid + 1 + + return lo - 1 +``` + +https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function 的多语言解法👆 + +https://leetcode.cn/problems/product-of-the-last-k-numbers 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class ProductOfNumbers { + // 前缀积数组 + // preProduct[i] / preProduct[j] 就是 [i, j] 之间的元素积 + vector preProduct; + +public: + ProductOfNumbers() { + // 初始化放一个 1,便于计算后续添加元素的乘积 + preProduct.push_back(1); + } + + void add(int num) { + if (num == 0) { + // 如果添加的元素是 0,则前面的元素积都废了 + preProduct.clear(); + preProduct.push_back(1); + return; + } + int n = preProduct.size(); + // 前缀积数组中每个元素 + preProduct.push_back(preProduct[n - 1] * num); + } + + int getProduct(int k) { + int n = preProduct.size(); + if (k > n - 1) { + // 不足 k 个元素,是因为最后 k 个元素存在 0 + return 0; + } + // 计算最后 k 个元素积 + return preProduct[n - 1] / preProduct[n - k - 1]; + } +}; +``` + +```go +// by chatGPT (go) +type ProductOfNumbers struct { + // 前缀积数组 + // preProduct[i] / preProduct[j] 就是 [i, j] 之间的元素积 + preProduct []int +} + +func Constructor() ProductOfNumbers { + // 初始化放一个 1,便于计算后续添加元素的乘积 + return ProductOfNumbers{[]int{1}} +} + +func (this *ProductOfNumbers) Add(num int) { + if num == 0 { + // 如果添加的元素是 0,则前面的元素积都废了 + this.preProduct = []int{1} + return + } + n := len(this.preProduct) + // 前缀积数组中每个元素 + this.preProduct = append(this.preProduct, this.preProduct[n-1]*num) +} + +func (this *ProductOfNumbers) GetProduct(k int) int { + n := len(this.preProduct) + if k > n-1 { + // 不足 k 个元素,是因为最后 k 个元素存在 0 + return 0 + } + // 计算最后 k 个元素积 + return this.preProduct[n-1] / this.preProduct[n-k-1] +} +``` + +```java +// by labuladong (java) +class ProductOfNumbers { + // 前缀积数组 + // preProduct[i] / preProduct[j] 就是 [i, j] 之间的元素积 + ArrayList preProduct = new ArrayList<>(); + + public ProductOfNumbers() { + // 初始化放一个 1,便于计算后续添加元素的乘积 + preProduct.add(1); + } + + public void add(int num) { + if (num == 0) { + // 如果添加的元素是 0,则前面的元素积都废了 + preProduct.clear(); + preProduct.add(1); + return; + } + int n = preProduct.size(); + // 前缀积数组中每个元素 + preProduct.add(preProduct.get(n - 1) * num); + } + + public int getProduct(int k) { + int n = preProduct.size(); + if (k > n - 1) { + // 不足 k 个元素,是因为最后 k 个元素存在 0 + return 0; + } + // 计算最后 k 个元素积 + return preProduct.get(n - 1) / preProduct.get(n - k - 1); + } +} +``` + +```javascript +// by chatGPT (javascript) +var ProductOfNumbers = function() { + // 前缀积数组 + // preProduct[i] / preProduct[j] 就是 [i, j] 之间的元素积 + this.preProduct = [1]; +}; + +ProductOfNumbers.prototype.add = function(num) { + if (num === 0) { + // 如果添加的元素是 0,则前面的元素积都废了 + this.preProduct = [1]; + return; + } + var n = this.preProduct.length; + // 前缀积数组中每个元素 + this.preProduct.push(this.preProduct[n - 1] * num); +}; + +ProductOfNumbers.prototype.getProduct = function(k) { + var n = this.preProduct.length; + if (k > n - 1) { + // 不足 k 个元素,是因为最后 k 个元素存在 0 + return 0; + } + // 计算最后 k 个元素积 + return this.preProduct[n - 1] / this.preProduct[n - k - 1]; +}; +``` + +```python +# by chatGPT (python) +class ProductOfNumbers: + # 前缀积数组 + # preProduct[i] / preProduct[j] 就是 [i, j] 之间的元素积 + def __init__(self): + self.preProduct = [1] + + def add(self, num: int) -> None: + if num == 0: + # 如果添加的元素是 0,则前面的元素积都废了 + self.preProduct.clear() + self.preProduct.append(1) + return + n = len(self.preProduct) + # 前缀积数组中每个元素 + self.preProduct.append(self.preProduct[n - 1] * num) + + def getProduct(self, k: int) -> int: + n = len(self.preProduct) + if k > n - 1: + # 不足 k 个元素,是因为最后 k 个元素存在 0 + return 0 + # 计算最后 k 个元素积 + return self.preProduct[n - 1] // self.preProduct[n - k - 1] +``` + +https://leetcode.cn/problems/product-of-the-last-k-numbers 的多语言解法👆 + +https://leetcode.cn/problems/qJnOS7 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int longestCommonSubsequence(string s1, string s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + vector> dp(m + 1, vector(n + 1, 0)); + // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + // base case: dp[0][..] = dp[..][0] = 0 + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1[i - 1] == s2[j - 1]) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + + return dp[m][n]; + } +}; +``` + +```go +// by chatGPT (go) +func longestCommonSubsequence(s1 string, s2 string) int { + m, n := len(s1), len(s2) + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + dp := make([][]int, m+1) + for i := 0; i <= m; i++ { + dp[i] = make([]int, n+1) + } + // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + // base case: dp[0][..] = dp[..][0] = 0 + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + // 现在 i 和 j 从 1 开始,所以要减一 + if s1[i-1] == s2[j-1] { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i-1][j-1] + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j-1], dp[i-1][j]) + } + } + } + + return dp[m][n] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int longestCommonSubsequence(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + int[][] dp = new int[m + 1][n + 1]; + // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + // base case: dp[0][..] = dp[..][0] = 0 + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 现在 i 和 j 从 1 开始,所以要减一 + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + // s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + + return dp[m][n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var longestCommonSubsequence = function(s1, s2) { + var m = s1.length, n = s2.length; + // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + var dp = new Array(m + 1); + for(var i = 0;i int: + m, n = len(s1), len(s2) + # 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j] + dp = [[0] * (n + 1) for _ in range(m + 1)] + # 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n] + # base case: dp[0][..] = dp[..][0] = 0 + + for i in range(1, m + 1): + for j in range(1, n + 1): + # 现在 i 和 j 从 1 开始,所以要减一 + if s1[i - 1] == s2[j - 1]: + # s1[i-1] 和 s2[j-1] 必然在 lcs 中 + dp[i][j] = 1 + dp[i - 1][j - 1] + else: + # s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中 + dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]) + + return dp[m][n] +``` + +https://leetcode.cn/problems/qJnOS7 的多语言解法👆 + +https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 备忘录 + vector memo; + int MOD = 1000000007; + +public: + int numWays(int n) { + if (n == 0) { + return 1; + } + memo = vector(n + 1, 0); + return dp(n); + } + + // 定义:爬到第 n 级台阶的方法个数为 dp(n) + int dp(int n) { + // base case + if (n <= 2) { + return n; + } + if (memo[n] > 0) { + return memo[n]; + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = (dp(n - 1) + dp(n - 2)) % MOD; + return memo[n]; + } +}; +``` + +```go +// by chatGPT (go) +func numWays(n int) int { + memo := make([]int, n+1) + + var dp func(int) int + dp = func(n int) int { + // base case + if n <= 2 { + return n + } + if memo[n] > 0 { + return memo[n] + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = dp(n-1) + dp(n-2) + return memo[n] + } + + return dp(n) +} +``` + +```java +// by labuladong (java) +class Solution { + // 备忘录 + int[] memo; + + public int climbStairs(int n) { + memo = new int[n + 1]; + return dp(n); + } + + // 定义:爬到第 n 级台阶的方法个数为 dp(n) + int dp(int n) { + // base case + if (n <= 2) { + return n; + } + if (memo[n] > 0) { + return memo[n]; + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = dp(n - 1) + dp(n - 2); + return memo[n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number} + */ +var numWays = function(n) { + // 备忘录 + const memo = []; + memo.length = n + 1; + memo.fill(0); + return dp(n, memo); +}; + +/** + * @param {number} n + * @param {number[]} memo + * @return {number} + */ +var dp = function(n, memo) { + // base case + if (n <= 2) { + return n; + } + if (memo[n] > 0) { + return memo[n]; + } + // 状态转移方程: + // 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + memo[n] = dp(n - 1, memo) + dp(n - 2, memo); + return memo[n]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + # 备忘录 + memo = [] + MOD = 1e9 + 7 + def numWays(self, n: int) -> int: + self.memo = [-1] * (n + 1) + return self.dp(n) + + # 定义:爬到第 n 级台阶的方法个数为 dp(n) + def dp(self, n: int) -> int: + # base case + if n <= 2: + return n if n else 1 + + if self.memo[n] != -1: + return self.memo[n] + + # 状态转移方程: + # 爬到第 n 级台阶的方法个数等于爬到 n - 1 的方法个数和爬到 n - 2 的方法个数之和。 + self.memo[n] = self.dp(n - 1) + self.dp(n - 2) + return round(self.memo[n] % self.MOD) +``` + +https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof 的多语言解法👆 + +https://leetcode.cn/problems/qn8gGX 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> kSmallestPairs(vector& nums1, vector& nums2, int k) { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + auto cmp = [](auto & a, auto & b) { + return a[0] + a[1] > b[0] + b[1]; + }; + priority_queue, vector>, decltype(cmp)> pq(cmp); + // 按照 23 题的逻辑初始化优先级队列 + for (int i = 0; i < nums1.size(); i++) { + pq.push({nums1[i], nums2[0], 0}); + } + + vector> res; + // 执行合并多个有序链表的逻辑 + while (!pq.empty() && k > 0) { + auto cur = pq.top(); + pq.pop(); + k--; + // 链表中的下一个节点加入优先级队列 + int next_index = cur[2] + 1; + if (next_index < nums2.size()) { + pq.push({cur[0], nums2[next_index], next_index}); + } + + vector pair; + pair.push_back(cur[0]); + pair.push_back(cur[1]); + res.push_back(pair); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "container/heap" + "fmt" +) + +type item struct { + num1 int + num2 int + num2Index int +} + +// 用于转换 item 为 heap 中实际存储的数据 +type itemHeap []item + +func (h itemHeap) Len() int { return len(h) } +func (h itemHeap) Less(i, j int) bool { + return h[i].num1 + h[i].num2 < h[j].num1 + h[j].num2 +} +func (h itemHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h *itemHeap) Push(x interface{}) { *h = append(*h, x.(item)) } +func (h *itemHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[:n-1] + return x +} + +func kSmallestPairs(nums1 []int, nums2 []int, k int) [][]int { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + pq := &itemHeap{} + heap.Init(pq) + + // 按照23题的逻辑初始化优先级队列 + for i := 0; i < len(nums1); i++ { + heap.Push(pq, item{nums1[i], nums2[0], 0}) + } + + res := [][]int{} + // 执行合并多个有序链表的逻辑 + for pq.Len() > 0 && k > 0 { + cur := heap.Pop(pq).(item) + k-- + // 链表中的下一个节点加入优先级队列 + nextIndex := cur.num2Index + 1 + if nextIndex < len(nums2) { + heap.Push(pq, item{cur.num1, nums2[nextIndex], nextIndex}) + } + + pair := []int{cur.num1, cur.num2} + res = append(res, pair) + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public List> kSmallestPairs(int[] nums1, int[] nums2, int k) { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + PriorityQueue pq = new PriorityQueue<>((a, b) -> { + // 按照数对的元素和升序排序 + return (a[0] + a[1]) - (b[0] + b[1]); + }); + // 按照 23 题的逻辑初始化优先级队列 + for (int i = 0; i < nums1.length; i++) { + pq.offer(new int[]{nums1[i], nums2[0], 0}); + } + + List> res = new ArrayList<>(); + // 执行合并多个有序链表的逻辑 + while (!pq.isEmpty() && k > 0) { + int[] cur = pq.poll(); + k--; + // 链表中的下一个节点加入优先级队列 + int next_index = cur[2] + 1; + if (next_index < nums2.length) { + pq.add(new int[]{cur[0], nums2[next_index], next_index}); + } + + List pair = new ArrayList<>(); + pair.add(cur[0]); + pair.add(cur[1]); + res.add(pair); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums1 + * @param {number[]} nums2 + * @param {number} k + * @return {number[][]} + */ + +var kSmallestPairs = function(nums1, nums2, k) { + // 存储三元组 (num1[i], nums2[i], i) + // i 记录 nums2 元素的索引位置,用于生成下一个节点 + const pq = new PriorityQueue((a, b) => { + // 按照数对的元素和升序排序 + return (a[0] + a[1]) - (b[0] + b[1]); + }); + // 按照 23 题的逻辑初始化优先级队列 + for (let i = 0; i < nums1.length; i++) { + pq.offer([nums1[i], nums2[0], 0]); + } + + const res = []; + // 执行合并多个有序链表的逻辑 + while (!pq.isEmpty() && k > 0) { + const cur = pq.poll(); + k--; + // 链表中的下一个节点加入优先级队列 + const next_index = cur[2] + 1; + if (next_index < nums2.length) { + pq.add([cur[0], nums2[next_index], next_index]); + } + + const pair = []; + pair.push(cur[0]); + pair.push(cur[1]); + res.push(pair); + } + return res; +}; + +// An implementation of the PriorityQueue. +class PriorityQueue { + constructor(compare = (a, b) => a - b) { + this.compare = compare; + this.heap = []; + } + get size() { + return this.heap.length; + } + isEmpty() { + return this.size === 0; + } + peek() { + return this.heap[0]; + } + offer(node) { + this.heap.push(node); + this._siftUp(); + } + poll() { + const poppedValue = this.peek(); + const bottom = this.size - 1; + if (bottom > 0) { + this._swap(0, bottom); + } + this.heap.pop(); + this._siftDown(); + return poppedValue; + } + _greater(i, j) { + return this.compare(this.heap[i], this.heap[j]) < 0; + } + _swap(i, j) { + [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]; + } + _siftUp() { + let nodeIdx = this.size - 1; + while (nodeIdx > 0 && this._greater(nodeIdx, Math.floor((nodeIdx + 1) / 2) - 1)) { + const parentIdx = Math.floor((nodeIdx + 1) / 2) - 1; + this._swap(nodeIdx, parentIdx); + nodeIdx = parentIdx; + } + } + _siftDown() { + let nodeIdx = 0; + while ( + (2 * nodeIdx + 1 < this.size && this._greater(2 * nodeIdx + 1, nodeIdx)) || + (2 * nodeIdx + 2 < this.size && this._greater(2 * nodeIdx + 2, nodeIdx)) + ) { + const greaterChildIdx = + 2 * nodeIdx + 2 >= this.size || this._greater(2 * nodeIdx + 1, 2 * nodeIdx + 2) + ? 2 * nodeIdx + 1 + : 2 * nodeIdx + 2; + this._swap(greaterChildIdx, nodeIdx); + nodeIdx = greaterChildIdx; + } + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]: + # 存储三元组 (num1[i], nums2[i], i) + # i 记录 nums2 元素的索引位置,用于生成下一个节点 + pq = [] + for i in range(len(nums1)): + heapq.heappush(pq, [nums1[i]+nums2[0], nums1[i], nums2[0], 0]) + + res = [] + while pq and k > 0: + _, num1, num2, idx2 = heapq.heappop(pq) + res.append([num1, num2]) + k -= 1 + if idx2 < len(nums2)-1: + heapq.heappush(pq, [num1+nums2[idx2+1], num1, nums2[idx2+1], idx2+1]) + return res +``` + +https://leetcode.cn/problems/qn8gGX 的多语言解法👆 + +https://leetcode.cn/problems/que-shi-de-shu-zi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int missingNumber(vector& nums) { + // 搜索左侧的二分搜索 + int left = 0, right = nums.size() - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] > mid) { + // mid 和 nums[mid] 不对应,说明左边有元素缺失 + right = mid - 1; + } else { + // mid 和 nums[mid] 对应,说明元素缺失在右边 + left = mid + 1; + } + } + return left; + } +}; +``` + +```go +// by chatGPT (go) +// missingNumber returns the missing number in the given array using binary search +func missingNumber(nums []int) int { + // 搜索左侧的二分搜索 + left, right := 0, len(nums)-1 + + for left <= right { + mid := left + (right - left) / 2 + if nums[mid] > mid { + // mid 和 nums[mid] 不对应,说明左边有元素缺失 + right = mid - 1 + } else { + // mid 和 nums[mid] 对应,说明元素缺失在右边 + left = mid + 1 + } + } + + return left +} +``` + +```java +// by labuladong (java) +class Solution { + public int missingNumber(int[] nums) { + // 搜索左侧的二分搜索 + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] > mid) { + // mid 和 nums[mid] 不对应,说明左边有元素缺失 + right = mid - 1; + } else { + // mid 和 nums[mid] 对应,说明元素缺失在右边 + left = mid + 1; + } + } + return left; + } +} +``` + +```javascript +// by chatGPT (javascript) +var missingNumber = function(nums) { + // 搜索左侧的二分搜索 + var left = 0, right = nums.length - 1; + while (left <= right) { + var mid = left + Math.floor((right - left) / 2); + if (nums[mid] > mid) { + // mid 和 nums[mid] 不对应,说明左边有元素缺失 + right = mid - 1; + } else { + // mid 和 nums[mid] 对应,说明元素缺失在右边 + left = mid + 1; + } + } + return left; +} +``` + +```python +# by chatGPT (python) +class Solution: + def missingNumber(self, nums: List[int]) -> int: + # 搜索左侧的二分搜索 + left, right = 0, len(nums) - 1 + while left <= right: + mid = left + (right - left) // 2 + if nums[mid] > mid: + # mid 和 nums[mid] 不对应,说明左边有元素缺失 + right = mid - 1 + else: + # mid 和 nums[mid] 对应,说明元素缺失在右边 + left = mid + 1 + return left +``` + +https://leetcode.cn/problems/que-shi-de-shu-zi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/random-flip-matrix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + int m, n; + // 抽象一维数组的长度 + int len; + // 已删除元素到尾部未删除元素的映射 + unordered_map deletedToExist; + // 这个 deletedToExist 存的到底是什么呢?举个例子你就明白了: + // 假设数组 [1,2,3,4,5],我第一次随机选择了元素 2, + // 然后我就会在 deletedToExist 记录一个映射 2 -> 5,并删除最后的元素 5 + // 现在数组是 [1,2,3,4],我第二次随机选择又选到了 2, + // 但不能重复选择同一个元素,所以我选择 5,因为 deletedToExist[2] = 5 + // 然后我再更新 deletedToExist,记录 2 -> 4 并删除最后的元素 4 + // 以此类推,这样就能保证随机选择到相同的元素时也能等概率地返回不同的元素 + + mt19937 rand_engine = mt19937(time(nullptr)); + +public: + Solution(int m, int n) { + this->m = m; + this->n = n; + this->len = m * n; + this->deletedToExist = unordered_map(); + } + + vector flip() { + int rand_num = rand_engine() % len; + // 这个随机数可能已经被删掉了(刚才已经被选过) + int res = rand_num; + if (deletedToExist.count(rand_num)) { + res = deletedToExist[rand_num]; + } + // 把 rand 置换到数组尾部 + int last = len - 1; + // 尾部的那个元素也可能已经被删掉了 + if (deletedToExist.count(last)) { + last = deletedToExist[last]; + } + deletedToExist[rand_num] = last; + // 把尾部的这个元素删掉 + len--; + // 一维坐标转化成二维坐标 + return {res / n, res % n}; + } + + void reset() { + this->len = this->m * this->n; + this->deletedToExist.clear(); + } +}; +``` + +```go +// by chatGPT (go) +type Solution struct { + m, n int + // 抽象一维数组的长度 + len int + // 已删除元素到尾部未删除元素的映射 + deletedToExist map[int]int + // 这个 deletedToExist 存的到底是什么呢?举个例子你就明白了: + // 假设数组 [1,2,3,4,5],我第一次随机选择了元素 2, + // 然后我就会在 deletedToExist 记录一个映射 2 -> 5,并删除最后的元素 5 + // 现在数组是 [1,2,3,4],我第二次随机选择又选到了 2, + // 但不能重复选择同一个元素,所以我选择 5,因为 deletedToExist[2] = 5 + // 然后我再更新 deletedToExist,记录 2 -> 4 并删除最后的元素 4 + // 以此类推,这样就能保证随机选择到相同的元素时也能等概率地返回不同的元素 + random *rand.Rand +} + +func Constructor(m int, n int) Solution { + return Solution{ + m: m, + n: n, + len: m * n, + deletedToExist: make(map[int]int), + random: rand.New(rand.NewSource(time.Now().Unix())), + } +} + +func (this *Solution) Flip() []int { + rand := this.random.Intn(this.len) + // 这个随机数可能已经被删掉了(刚才已经被选过) + res := rand + if val, ok := this.deletedToExist[rand]; ok { + res = val + } + // 把 rand 置换到数组尾部 + last := this.len - 1 + // 尾部的那个元素也可能已经被删掉了 + if val, ok := this.deletedToExist[last]; ok { + last = val + } + this.deletedToExist[rand] = last + // 把尾部的这个元素删掉 + this.len-- + // 一维坐标转化成二维坐标 + return []int{res / this.n, res % this.n} +} + +func (this *Solution) Reset() { + this.len = this.m * this.n + this.deletedToExist = make(map[int]int) +} +``` + +```java +// by labuladong (java) +class Solution { + + int m, n; + // 抽象一维数组的长度 + int len; + // 已删除元素到尾部未删除元素的映射 + HashMap deletedToExist; + // 这个 deletedToExist 存的到底是什么呢?举个例子你就明白了: + // 假设数组 [1,2,3,4,5],我第一次随机选择了元素 2, + // 然后我就会在 deletedToExist 记录一个映射 2 -> 5,并删除最后的元素 5 + // 现在数组是 [1,2,3,4],我第二次随机选择又选到了 2, + // 但不能重复选择同一个元素,所以我选择 5,因为 deletedToExist[2] = 5 + // 然后我再更新 deletedToExist,记录 2 -> 4 并删除最后的元素 4 + // 以此类推,这样就能保证随机选择到相同的元素时也能等概率地返回不同的元素 + + Random random = new Random(); + + public Solution(int m, int n) { + this.m = m; + this.n = n; + this.len = m * n; + this.deletedToExist = new HashMap<>(); + } + + public int[] flip() { + int rand = random.nextInt(len); + // 这个随机数可能已经被删掉了(刚才已经被选过) + int res = rand; + if (deletedToExist.containsKey(rand)) { + res = deletedToExist.get(rand); + } + // 把 rand 置换到数组尾部 + int last = len - 1; + // 尾部的那个元素也可能已经被删掉了 + if (deletedToExist.containsKey(last)) { + last = deletedToExist.get(last); + } + deletedToExist.put(rand, last); + // 把尾部的这个元素删掉 + len--; + // 一维坐标转化成二维坐标 + return new int[]{res / n, res % n}; + } + + public void reset() { + this.len = this.m * this.n; + this.deletedToExist.clear(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} m + * @param {number} n + */ +var Solution = function(m, n) { + this.m = m; + this.n = n; + //抽象一维数组的长度 + this.len = m * n; + //已删除元素到尾部未删除元素的映射 + this.deletedToExist = new Map(); + //这个deletedToExist存的到底是什么呢?举个例子你就明白了: + //假设数组[1,2,3,4,5],我第一次随机选择了元素2, + //然后我就会在deletedToExist记录一个映射2->5,并删除最后的元素5 + //现在数组是[1,2,3,4],我第二次随机选择又选到了2, + //但不能重复选择同一个元素,所以我选择5,因为deletedToExist[2]=5 + //然后我再更新deletedToExist,记录2->4并删除最后的元素4 + //以此类推,这样就能保证随机选择到相同的元素时也能等概率地返回不同的元素 + this.random = Math.random; +}; + +/** + * @return {number[]} + */ +Solution.prototype.flip = function() { + let rand = Math.floor(this.random() * this.len); + //这个随机数可能已经被删掉了(刚才已经被选过) + let res = rand; + if (this.deletedToExist.has(rand)) { + res = this.deletedToExist.get(rand); + } + //把rand置换到数组尾部 + let last = this.len - 1; + //尾部的那个元素也可能已经被删掉了 + if (this.deletedToExist.has(last)) { + last = this.deletedToExist.get(last); + } + this.deletedToExist.set(rand, last); + //把尾部的这个元素删掉 + this.len--; + //一维坐标转化成二维坐标 + return [Math.floor(res / this.n), res % this.n]; +}; + +Solution.prototype.reset = function() { + this.len = this.m * this.n; + this.deletedToExist.clear(); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self, m: int, n: int): + self.m = m # 矩阵的行数 + self.n = n # 矩阵的列数 + self.len = m * n # 抽象一维数组的长度 + # 已删除元素到尾部未删除元素的映射 + self.deletedToExist = {} + + def flip(self) -> List[int]: + rand = random.randint(0, self.len - 1) # 随机选择一个元素 + res = rand + # 这个随机数可能已经被删掉了(刚才已经被选过) + if rand in self.deletedToExist: + res = self.deletedToExist[rand] + # 把随机选择的元素置换到数组尾部 + last = self.len - 1 + # 尾部的那个元素也可能已经被删掉了 + if last in self.deletedToExist: + last = self.deletedToExist[last] + self.deletedToExist[rand] = last + # 把尾部的元素标记为已删除 + self.len -= 1 + # 返回一维数组中索引为 res 的元素的二维坐标 + return [res // self.n, res % self.n] + + def reset(self) -> None: + self.len = self.m * self.n # 重置抽象一维数组的长度 + self.deletedToExist.clear() # 清空已删除元素到尾部未删除元素的映射 +``` + +https://leetcode.cn/problems/random-flip-matrix 的多语言解法👆 + +https://leetcode.cn/problems/random-pick-index 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +private: + vector nums; +public: + Solution(vector& nums) { + this->nums = nums; + } + + int pick(int target) { + int count = 0, res = -1; + for (int i = 0; i < nums.size(); i++) { + if (nums[i] != target) { + continue; + } + count++; + if (rand() % count == 0) { + res = i; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +// Constructor function that receives a nums array and returns a solution struct +func Constructor(nums []int) Solution { + randSource := rand.NewSource(time.Now().UnixNano()) + randGen := rand.New(randSource) + return Solution{nums, randGen} +} + +// Pick function receives a target value and returns the index of the element in the nums array +func Pick(s *Solution, target int) int { + count := 0 + res := -1 + for i := 0; i < len(s.nums); i++ { + if s.nums[i] != target { + continue + } + count++ + if s.rand.Intn(count) == 0 { + res = i + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + int[] nums; + Random rand; + + public Solution(int[] nums) { + this.nums = nums; + this.rand = new Random(); + } + + public int pick(int target) { + int count = 0, res = -1; + for (int i = 0; i < nums.length; i++) { + if (nums[i] != target) { + continue; + } + count++; + if (rand.nextInt(count) == 0) { + res = i; + } + } + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + */ +var Solution = function(nums) { + this.nums = nums; + this.rand = new Random(); +}; + +/** + * @param {number} target + * @return {number} + */ +Solution.prototype.pick = function(target) { + let count = 0; + let res = -1; + for (let i = 0; i < this.nums.length; i++) { + if (this.nums[i] !== target) { + continue; + } + count++; + if (this.rand.nextInt(count) === 0) { + res = i; + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +import random + +class Solution: + def __init__(self, nums: List[int]): + self.nums = nums + self.rand = random.Random() + + def pick(self, target: int) -> int: + count, res = 0, -1 + for i in range(len(self.nums)): + if self.nums[i] != target: + continue + count += 1 + if self.rand.randint(1, count) == 1: + res = i + return res +``` + +https://leetcode.cn/problems/random-pick-index 的多语言解法👆 + +https://leetcode.cn/problems/random-pick-with-blacklist 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { +public: + int sz; + unordered_map mapping; + + Solution(int N, vector& blacklist) { + sz = N - blacklist.size(); + for (int b : blacklist) { + mapping[b] = 666; + } + + int last = N - 1; + for (int b : blacklist) { + // 如果 b 已经在区间 [sz, N) + // 可以直接忽略 + if (b >= sz) { + continue; + } + while (mapping.count(last)) { + last--; + } + mapping[b] = last; + last--; + } + } + + int pick() { + // 随机选取一个索引 + int index = rand() % sz; + // 这个索引命中了黑名单, + // 需要被映射到其他位置 + if (mapping.count(index)) { + return mapping[index]; + } + // 若没命中黑名单,则直接返回 + return index; + } +}; +``` + +```go +// by chatGPT (go) +type Solution struct { + sz int + mapping map[int]int +} + +// 构造函数,初始化黑名单映射表 +// N: [0, N) 中的数字 +// blacklist: 黑名单中的数字 +func Constructor(N int, blacklist []int) Solution { + s := Solution{} + s.sz = N - len(blacklist) + s.mapping = make(map[int]int) + + for _, b := range blacklist { + s.mapping[b] = 666 + } + + last := N - 1 + for _, b := range blacklist { + // 已经在区间 [sz, N) 的数字可以直接忽略 + if b >= s.sz { + continue + } + // 找到可以映射到b的值 + for _, ok := s.mapping[last]; ok; _, ok = s.mapping[last] { + last-- + } + s.mapping[b] = last + last-- + } + + return s +} + +// 从集合中随机选择一个不在黑名单中的数字 +func (this *Solution) Pick() int { + index := rand.Intn(this.sz) + if val, ok := this.mapping[index]; ok { + return val + } + return index +} +``` + +```java +// by chatGPT (java) +class Solution { + int sz; + Map mapping; + + public Solution(int N, int[] blacklist) { + sz = N - blacklist.length; + mapping = new HashMap<>(); + + for (int b : blacklist) { + mapping.put(b, 666); // 标记黑名单 + } + + int last = N - 1; + for (int b : blacklist) { + // 如果 b 已经在区间 [sz, N),可以直接忽略 + if (b >= sz) { + continue; + } + while (mapping.containsKey(last)) { // 找到可以映射的位置 + last--; + } + mapping.put(b, last); // 映射 + last--; + } + } + + public int pick() { + // 随机选取一个索引 + int index = (int)(Math.random() * sz); + // 这个索引命中了黑名单,需要被映射到其他位置 + if (mapping.containsKey(index)) { + return mapping.get(index); + } + // 若没命中黑名单,则直接返回 + return index; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} N + * @param {number[]} blacklist + */ +var Solution = function(N, blacklist) { + // 求出白名单的长度 + this.sz = N - blacklist.length; + // 创建一个字典,用于快速查找黑名单中的元素 + this.mapping = {}; + // 将所有黑名单中的元素标记为 666 + blacklist.forEach((b) => { + this.mapping[b] = 666; + }); + // 遍历黑名单中的元素,为之给定一个新的位置 + let last = N - 1; + // 注意遍历的顺序,需要将最后的黑名单尽量映射到白名单前面的位置 + blacklist.forEach((b) => { + // 如果该黑名单元素已经在白名单的范围内了,则不需要进行映射 + if (b >= this.sz) { + return; + } + // 找到还未被映射的最大的位置 + while (this.mapping.hasOwnProperty(last)) { + last--; + } + // 为当前黑名单元素 b 指定一个新的位置 + this.mapping[b] = last; + last--; + }); +}; + +/** + * @return {number} + */ +Solution.prototype.pick = function() { + // 随机选取一个索引值 + let index = Math.floor(Math.random() * this.sz); + // 如果该索引位是黑名单中的元素,则将其映射到新的位置上 + if (this.mapping.hasOwnProperty(index)) { + return this.mapping[index]; + } + // 否则直接返回该索引值 + return index; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self, N: int, blacklist: List[int]): + # 需要返回的数字范围为[0, sz) + self.sz = N - len(blacklist) + # 将黑名单中的数字映射到666,即避免选中 + self.mapping = {} + for b in blacklist: + self.mapping[b] = 666 + + # 对于黑名单中的数字,如果其在[0, sz)范围内, + # 且不在mapping中,对其进行重新映射 + last = N - 1 + for b in blacklist: + if b >= self.sz: + continue + # 在映射中查找不在黑名单中,且小于N的最大的数last + while last in self.mapping: + last -= 1 + # 将b映射到last + self.mapping[b] = last + last -= 1 + + def pick(self) -> int: + # 随机选择一个数字 + index = random.randint(0, self.sz-1) + # 如果所选数字在映射中,返回其映射值 + if index in self.mapping: + return self.mapping[index] + # 如果所选数字不在映射中,直接返回 + return index +``` + +https://leetcode.cn/problems/random-pick-with-blacklist 的多语言解法👆 + +https://leetcode.cn/problems/range-addition 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector getModifiedArray(int length, vector>& updates) { + // nums 初始化为全 0 + vector nums(length, 0); + // 构造差分解法 + Difference df(nums); + for (auto update : updates) { + int i = update[0]; + int j = update[1]; + int val = update[2]; + df.increment(i, j, val); + } + return df.result(); + } + + class Difference { + private: + // 差分数组 + vector diff; + + public: + Difference(vector& nums) { + assert(nums.size() > 0); + diff.resize(nums.size()); + // 构造差分数组 + diff[0] = nums[0]; + for (int i = 1; i < nums.size(); i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.size()) { + diff[j + 1] -= val; + } + } + + vector result() { + vector res(diff.size()); + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (int i = 1; i < diff.size(); i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } + }; +}; +``` + +```go +// by chatGPT (go) +func getModifiedArray(length int, updates [][]int) []int { + // nums 初始化为全 0 + var nums = make([]int, length) + + // 构造差分解法 + df := Difference{nums} + for _, update := range updates { + i := update[0] + j := update[1] + val := update[2] + df.increment(i, j, val) + } + return df.result() +} + +type Difference struct { + // 差分数组 + diff []int +} + +func (df *Difference) increment(i, j, val int) { + df.diff[i] += val + if j+1 < len(df.diff) { + df.diff[j+1] -= val + } +} + +func (df *Difference) result() []int { + res := make([]int, len(df.diff)) + // 根据差分数组构造结果数组 + res[0] = df.diff[0] + for i := 1; i < len(df.diff); i++ { + res[i] = res[i-1] + df.diff[i] + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] getModifiedArray(int length, int[][] updates) { + // nums 初始化为全 0 + int[] nums = new int[length]; + // 构造差分解法 + Difference df = new Difference(nums); + for (int[] update : updates) { + int i = update[0]; + int j = update[1]; + int val = update[2]; + df.increment(i, j, val); + } + return df.result(); + } + + class Difference { + // 差分数组 + private int[] diff; + + public Difference(int[] nums) { + assert nums.length > 0; + diff = new int[nums.length]; + // 构造差分数组 + diff[0] = nums[0]; + for (int i = 1; i < nums.length; i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + public void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.length) { + diff[j + 1] -= val; + } + } + + public int[] result() { + int[] res = new int[diff.length]; + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (int i = 1; i < diff.length; i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } + } + +} +``` + +```javascript +// by chatGPT (javascript) +var getModifiedArray = function(length, updates) { + // nums 初始化为全 0 + var nums = new Array(length); + nums.fill(0); + // 构造差分解法 + var df = new Difference(nums); + for (var k = 0; k < updates.length; k++) { + var i = updates[k][0]; + var j = updates[k][1]; + var val = updates[k][2]; + df.increment(i, j, val); + } + return df.result(); +}; + +var Difference = function(nums) { + // 差分数组 + var diff = []; + if (nums.length > 0) { + // 构造差分数组 + diff[0] = nums[0]; + for (var i = 1; i < nums.length; i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + /* 给闭区间 [i, j] 增加 val(可以是负数)*/ + this.increment = function(i, j, val) { + diff[i] += val; + if (j + 1 < diff.length) { + diff[j + 1] -= val; + } + } + + this.result = function() { + var res = new Array(diff.length); + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (var i = 1; i < diff.length; i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def getModifiedArray(self, length: int, updates: List[List[int]]) -> List[int]: + # nums 初始化为全 0 + nums = [0]*length + # 构造差分解法 + df = Difference(nums) + for update in updates: + i = update[0] + j = update[1] + val = update[2] + df.increment(i, j, val) + return df.result() + + class Difference: + # 差分数组 + def __init__(self, nums: List[int]): + assert len(nums) > 0 + self.diff = [0]*len(nums) + # 构造差分数组 + self.diff[0] = nums[0] + for i in range(1, len(nums)): + self.diff[i] = nums[i] - nums[i - 1] + + # 给闭区间 [i, j] 增加 val(可以是负数) + def increment(self, i: int, j: int, val: int) -> None: + self.diff[i] += val + if j + 1 < len(self.diff): + self.diff[j + 1] -= val + + def result(self) -> List[int]: + res = [0]*len(self.diff) + # 根据差分数组构造结果数组 + res[0] = self.diff[0] + for i in range(1, len(self.diff)): + res[i] = res[i - 1] + self.diff[i] + return res +``` + +https://leetcode.cn/problems/range-addition 的多语言解法👆 + +https://leetcode.cn/problems/range-sum-query-2d-immutable 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class NumMatrix { +private: + // preSum[i][j] 记录矩阵 [0, 0, i, j] 的元素和 + vector> preSum; + +public: + NumMatrix(vector>& matrix) { + int m = matrix.size(), n = matrix[0].size(); + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = vector>(m + 1, vector(n + 1, 0)); + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1]; + } + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + int sumRegion(int x1, int y1, int x2, int y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]; + } +}; +``` + +```go +// by chatGPT (go) +type NumMatrix struct { + // preSum[i][j] 记录矩阵 [0, 0, i, j] 的元素和 + preSum [][]int +} + +func Constructor(matrix [][]int) NumMatrix { + m, n := len(matrix), len(matrix[0]) + if m == 0 || n == 0 { + return NumMatrix{} + } + // 构造前缀和矩阵 + preSum := make([][]int, m+1) + for i := 0; i <= m; i++ { + preSum[i] = make([]int, n+1) + } + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i-1][j-1] - preSum[i-1][j-1] + } + } + return NumMatrix{preSum: preSum} +} + +func (this *NumMatrix) SumRegion(x1 int, y1 int, x2 int, y2 int) int { + // 目标矩阵之和由四个相邻矩阵运算获得 + return this.preSum[x2+1][y2+1] - this.preSum[x1][y2+1] - this.preSum[x2+1][y1] + this.preSum[x1][y1] +} +``` + +```java +// by labuladong (java) +class NumMatrix { + // preSum[i][j] 记录矩阵 [0, 0, i-1, j-1] 的元素和 + private int[][] preSum; + + public NumMatrix(int[][] matrix) { + int m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1]; + } + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + public int sumRegion(int x1, int y1, int x2, int y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} matrix 矩阵 + */ +var NumMatrix = function(matrix) { + const m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return; + // preSum[i][j] 记录矩阵 [0, 0, i, j] 的元素和 + this.preSum = new Array(m + 1).fill().map(() => new Array(n + 1).fill(0)); + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + this.preSum[i][j] = this.preSum[i - 1][j] + this.preSum[i][j - 1] + matrix[i - 1][j - 1] - this.preSum[i - 1][j - 1]; + } + } +}; + +/** + * @param {number} x1 子矩阵左上角坐标x + * @param {number} y1 子矩阵左上角坐标y + * @param {number} x2 子矩阵右下角坐标x + * @param {number} y2 子矩阵右下角坐标y + * @return {number} 子矩阵元素和 + */ +NumMatrix.prototype.sumRegion = function(x1, y1, x2, y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return this.preSum[x2 + 1][y2 + 1] - this.preSum[x1][y2 + 1] - this.preSum[x2 + 1][y1] + this.preSum[x1][y1]; +}; +``` + +```python +# by chatGPT (python) +class NumMatrix: + # preSum[i][j] 记录矩阵 [0, 0, i, j] 的元素和 + def __init__(self, matrix: List[List[int]]): + m, n = len(matrix), len(matrix[0]) + if m == 0 or n == 0: + return + # 构造前缀和矩阵 + self.preSum = [[0] * (n + 1) for _ in range(m + 1)] + for i in range(1, m + 1): + for j in range(1, n + 1): + # 计算每个矩阵 [0, 0, i, j] 的元素和 + self.preSum[i][j] = self.preSum[i - 1][j] + self.preSum[i][j - 1] + matrix[i - 1][j - 1] - self.preSum[i - 1][j - 1] + + # 计算子矩阵 [x1, y1, x2, y2] 的元素和 + def sumRegion(self, x1: int, y1: int, x2: int, y2: int) -> int: + # 目标矩阵之和由四个相邻矩阵运算获得 + return self.preSum[x2 + 1][y2 + 1] - self.preSum[x1][y2 + 1] - self.preSum[x2 + 1][y1] + self.preSum[x1][y1] +``` + +https://leetcode.cn/problems/range-sum-query-2d-immutable 的多语言解法👆 + +https://leetcode.cn/problems/range-sum-query-immutable 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class NumArray { + // 前缀和数组 +private: + vector preSum; + +public: + /* 输入一个数组,构造前缀和 */ + NumArray(vector& nums) { + // preSum[0] = 0,便于计算累加和 + preSum.resize(nums.size() + 1); + // 计算 nums 的累加和 + for (int i = 1; i < preSum.size(); i++) { + preSum[i] = preSum[i - 1] + nums[i - 1]; + } + } + + /* 查询闭区间 [left, right] 的累加和 */ + int sumRange(int left, int right) { + return preSum[right + 1] - preSum[left]; + } +}; +``` + +```go +// by chatGPT (go) +type NumArray struct { + preSum []int +} + +/* 输入一个数组,构造前缀和 */ +func Constructor(nums []int) NumArray { + // preSum[0] = 0,便于计算累加和 + preSum := make([]int, len(nums)+1) + // 计算 nums 的累加和 + for i := 1; i < len(preSum); i++ { + preSum[i] = preSum[i-1] + nums[i-1] + } + return NumArray{preSum} +} + +/* 查询闭区间 [left, right] 的累加和 */ +func (this *NumArray) SumRange(left int, right int) int { + return this.preSum[right+1] - this.preSum[left] +} +``` + +```java +// by labuladong (java) +class NumArray { + // 前缀和数组 + private int[] preSum; + + /* 输入一个数组,构造前缀和 */ + public NumArray(int[] nums) { + // preSum[0] = 0,便于计算累加和 + preSum = new int[nums.length + 1]; + // 计算 nums 的累加和 + for (int i = 1; i < preSum.length; i++) { + preSum[i] = preSum[i - 1] + nums[i - 1]; + } + } + + /* 查询闭区间 [left, right] 的累加和 */ + public int sumRange(int left, int right) { + return preSum[right + 1] - preSum[left]; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + */ +var NumArray = function(nums) { + // 前缀和数组 + this.preSum = []; + // preSum[0] = 0,便于计算累加和 + this.preSum[0] = 0; + // 计算 nums 的累加和 + for (let i = 1; i <= nums.length; i++) { + this.preSum[i] = this.preSum[i - 1] + nums[i - 1]; + } +}; + +/** + * @param {number} left + * @param {number} right + * @return {number} + */ +NumArray.prototype.sumRange = function(left, right) { + return this.preSum[right + 1] - this.preSum[left]; +}; +``` + +```python +# by chatGPT (python) +class NumArray: + # 前缀和数组 + def __init__(self, nums: List[int]): + self.preSum = [0] * (len(nums) + 1) + for i in range(1, len(self.preSum)): + self.preSum[i] = self.preSum[i - 1] + nums[i - 1] + + # 查询闭区间 [left, right] 的累加和 + def sumRange(self, left: int, right: int) -> int: + return self.preSum[right + 1] - self.preSum[left] +``` + +https://leetcode.cn/problems/range-sum-query-immutable 的多语言解法👆 + +https://leetcode.cn/problems/regular-expression-matching 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + // 备忘录 + vector> memo; + + bool isMatch(string s, string p) { + int m = s.size(), n = p.size(); + memo = vector>(m, vector(n, -1)); + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0); + } + + /* 计算 p[j..] 是否匹配 s[i..] */ + bool dp(string& s, int i, string& p, int j) { + int m = s.size(), n = p.size(); + // base case + if (j == n) { + return i == m; + } + if (i == m) { + if ((n - j) % 2 == 1) { + return false; + } + for (; j + 1 < n; j += 2) { + if (p[j + 1] != '*') { + return false; + } + } + return true; + } + + // 查备忘录,防止重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + + bool res = false; + + if (s[i] == p[j] || p[j] == '.') { + if (j < n - 1 && p[j + 1] == '*') { + res = dp(s, i, p, j + 2) + || dp(s, i + 1, p, j); + } else { + res = dp(s, i + 1, p, j + 1); + } + } else { + if (j < n - 1 && p[j + 1] == '*') { + res = dp(s, i, p, j + 2); + } else { + res = false; + } + } + // 将当前结果记入备忘录 + memo[i][j] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +func isMatch(s string, p string) bool { + // 备忘录 + memo := make([][]int, len(s)) + for i := range memo { + memo[i] = make([]int, len(p)) + for j := range memo[i] { + memo[i][j] = -1 + } + } + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0, memo) +} + +/* 计算 p[j..] 是否匹配 s[i..] */ +func dp(s string, i int, p string, j int, memo [][]int) bool { + m, n := len(s), len(p) + // base case + if j == n { + return i == m + } + if i == m { + if (n-j)%2 == 1 { + return false + } + for ; j+1 < n; j += 2 { + if p[j+1] != '*' { + return false + } + } + return true + } + + // 查备忘录,防止重复计算 + if memo[i][j] != -1 { + return memo[i][j] == 1 + } + + var res bool + + if s[i] == p[j] || p[j] == '.' { + if j < n-1 && p[j+1] == '*' { + res = dp(s, i, p, j+2, memo) || dp(s, i+1, p, j, memo) + } else { + res = dp(s, i+1, p, j+1, memo) + } + } else { + if j < n-1 && p[j+1] == '*' { + res = dp(s, i, p, j+2, memo) + } else { + res = false + } + } + // 将当前结果记入备忘录 + memo[i][j] = 0 + if res { + memo[i][j] = 1 + } + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + // 备忘录 + int[][] memo; + + public boolean isMatch(String s, String p) { + int m = s.length(), n = p.length(); + memo = new int[m][n]; + for (int i = 0; i < m; i++) { + Arrays.fill(memo[i], -1); + } + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0); + } + + /* 计算 p[j..] 是否匹配 s[i..] */ + private boolean dp(String s, int i, String p, int j) { + int m = s.length(), n = p.length(); + // base case + if (j == n) { + return i == m; + } + if (i == m) { + if ((n - j) % 2 == 1) { + return false; + } + for (; j + 1 < n; j += 2) { + if (p.charAt(j + 1) != '*') { + return false; + } + } + return true; + } + + // 查备忘录,防止重复计算 + if (memo[i][j] != -1) { + return memo[i][j] == 1; + } + + boolean res = false; + + if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2) + || dp(s, i + 1, p, j); + } else { + res = dp(s, i + 1, p, j + 1); + } + } else { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2); + } else { + res = false; + } + } + // 将当前结果记入备忘录 + memo[i][j] = res ? 1 : 0; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isMatch = function(s, p) { + let m = s.length, n = p.length; + // 备忘录 + let memo = new Array(m + 1); + for (let i = 0; i < memo.length; i++) { + memo[i] = new Array(n + 1).fill(-1); + } + + return dp(0, 0); + + /* 计算 p[j..] 是否匹配 s[i..] */ + function dp(i, j) { + // 查备忘录,防止重复计算 + if (memo[i][j] !== -1) { + return memo[i][j]; + } + let res = false; + // base case + if (j === n) { + res = i === m; + } else { + let firstMatch = i < m && (p[j] === s[i] || p[j] === '.'); + if (j + 1 < n && p[j + 1] === '*') { + res = dp(i, j + 2) || (firstMatch && dp(i + 1, j)); + } else { + res = firstMatch && dp(i + 1, j + 1); + } + } + // 将当前结果记入备忘录 + memo[i][j] = res; + return res; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + # 备忘录 + memo = [] + + def isMatch(self, s: str, p: str) -> bool: + m, n = len(s), len(p) + self.memo = [[-1] * n for _ in range(m)] + # 指针 i,j 从索引 0 开始移动 + return self.dp(s, 0, p, 0) + + # 计算 p[j..] 是否匹配 s[i..] + def dp(self, s: str, i: int, p: str, j: int) -> bool: + m, n = len(s), len(p) + # base case + if j == n: + return i == m + if i == m: + if (n - j) % 2 == 1: + return False + for k in range(j + 1, n, 2): + if p[k] != '*': + return False + return True + + # 查备忘录,防止重复计算 + if self.memo[i][j] != -1: + return self.memo[i][j] + + res = False + + if s[i] == p[j] or p[j] == '.': + if j < n - 1 and p[j + 1] == '*': + res = self.dp(s, i, p, j + 2) or self.dp(s, i + 1, p, j) + else: + res = self.dp(s, i + 1, p, j + 1) + else: + if j < n - 1 and p[j + 1] == '*': + res = self.dp(s, i, p, j + 2) + else: + res = False + # 将当前结果记入备忘录 + self.memo[i][j] = res + return res +``` + +https://leetcode.cn/problems/regular-expression-matching 的多语言解法👆 + +https://leetcode.cn/problems/remove-covered-intervals 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int removeCoveredIntervals(vector>& intervals) { + // 按照起点升序排列,起点相同时降序排列 + sort(intervals.begin(), intervals.end(), [](vector& a, vector& b) { + if (a[0] == b[0]) { + return b[1] < a[1]; + } + return a[0] < b[0]; + }); + + // 记录合并区间的起点和终点 + int left = intervals[0][0]; + int right = intervals[0][1]; + + int res = 0; + for (int i = 1; i < intervals.size(); i++) { + vector& intv = intervals[i]; + // 情况一,找到覆盖区间 + if (left <= intv[0] && right >= intv[1]) { + res++; + } + // 情况二,找到相交区间,合并 + if (right >= intv[0] && right <= intv[1]) { + right = intv[1]; + } + // 情况三,完全不相交,更新起点和终点 + if (right < intv[0]) { + left = intv[0]; + right = intv[1]; + } + } + + return intervals.size() - res; + } +}; +``` + +```go +// by chatGPT (go) +import "sort" + +func removeCoveredIntervals(intervals [][]int) int { + // 按照起点升序排列,起点相同时降序排列 + sort.Slice(intervals, func(i, j int) bool { + if intervals[i][0] == intervals[j][0] { + return intervals[j][1] < intervals[i][1] + } + return intervals[i][0] < intervals[j][0] + }) + + // 记录合并区间的起点和终点 + left, right := intervals[0][0], intervals[0][1] + + res := 0 + for i := 1; i < len(intervals); i++ { + intv := intervals[i] + // 情况一,找到覆盖区间 + if left <= intv[0] && right >= intv[1] { + res++ + } + // 情况二,找到相交区间,合并 + if right >= intv[0] && right <= intv[1] { + right = intv[1] + } + // 情况三,完全不相交,更新起点和终点 + if right < intv[0] { + left, right = intv[0], intv[1] + } + } + + return len(intervals) - res +} +``` + +```java +// by labuladong (java) +class Solution { + public int removeCoveredIntervals(int[][] intervals) { + // 按照起点升序排列,起点相同时降序排列 + Arrays.sort(intervals, (a, b) -> { + if (a[0] == b[0]) { + return b[1] - a[1]; + } + return a[0] - b[0]; + }); + + // 记录合并区间的起点和终点 + int left = intervals[0][0]; + int right = intervals[0][1]; + + int res = 0; + for (int i = 1; i < intervals.length; i++) { + int[] intv = intervals[i]; + // 情况一,找到覆盖区间 + if (left <= intv[0] && right >= intv[1]) { + res++; + } + // 情况二,找到相交区间,合并 + if (right >= intv[0] && right <= intv[1]) { + right = intv[1]; + } + // 情况三,完全不相交,更新起点和终点 + if (right < intv[0]) { + left = intv[0]; + right = intv[1]; + } + } + + return intervals.length - res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} intervals + * @return {number} + */ +var removeCoveredIntervals = function(intervals) { + // 按照起点升序排列,起点相同时降序排列 + intervals.sort((a, b) => { + if (a[0] == b[0]) { + return b[1] - a[1]; + } + return a[0] - b[0]; + }); + + // 记录合并区间的起点和终点 + let left = intervals[0][0]; + let right = intervals[0][1]; + + let res = 0; + for (let i = 1; i < intervals.length; i++) { + const intv = intervals[i]; + // 情况一,找到覆盖区间 + if (left <= intv[0] && right >= intv[1]) { + res++; + } + // 情况二,找到相交区间,合并 + if (right >= intv[0] && right <= intv[1]) { + right = intv[1]; + } + // 情况三,完全不相交,更新起点和终点 + if (right < intv[0]) { + left = intv[0]; + right = intv[1]; + } + } + + return intervals.length - res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def removeCoveredIntervals(self, intervals: List[List[int]]) -> int: + """ + :type intervals: List[List[int]] + :rtype: int + """ + # 按照起点升序排列,起点相同时降序排列 + intervals.sort(key=lambda a: (a[0], -a[1])) + + # 记录合并区间的起点和终点 + left, right = intervals[0][0], intervals[0][1] + res = 0 + for i in range(1, len(intervals)): + intv = intervals[i] + # 情况一,找到覆盖区间 + if left <= intv[0] and right >= intv[1]: + res += 1 + # 情况二,找到相交区间,合并 + if right >= intv[0] and right <= intv[1]: + right = intv[1] + # 情况三,完全不相交,更新起点和终点 + if right < intv[0]: + left, right = intv[0], intv[1] + + return len(intervals) - res +``` + +https://leetcode.cn/problems/remove-covered-intervals 的多语言解法👆 + +https://leetcode.cn/problems/remove-duplicate-letters 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string removeDuplicateLetters(string s) { + stack stk; + + // 维护一个计数器记录字符串中字符的数量 + // 因为输入为 ASCII 字符,大小 256 够用了 + int count[256] = {0}; + for (int i = 0; i < s.length(); i++) { + count[s[i]]++; + } + + bool inStack[256] = {false}; + for (char c : s) { + // 每遍历过一个字符,都将对应的计数减一 + count[c]--; + + if (inStack[c]) continue; + + while (!stk.empty() && stk.top() > c) { + // 若之后不存在栈顶元素了,则停止 pop + if (count[stk.top()] == 0) { + break; + } + // 若之后还有,则可以 pop + inStack[stk.top()] = false; + stk.pop(); + } + stk.push(c); + inStack[c] = true; + } + + string res; + while (!stk.empty()) { + res.push_back(stk.top()); + stk.pop(); + } + reverse(res.begin(), res.end()); + return res; + } +}; +``` + +```go +// by chatGPT (go) +func removeDuplicateLetters(s string) string { + stk := []rune{} + + // 维护一个计数器记录字符串中字符的数量 + // 因为输入为 ASCII 字符,大小 256 够用了 + count := [256]int{} + for _, c := range s { + count[c]++ + } + + inStack := [256]bool{} + for _, c := range s { + // 每遍历过一个字符,都将对应的计数减一 + count[c]-- + + if inStack[c] { + continue + } + + for len(stk) > 0 && stk[len(stk)-1] > c { + // 若之后不存在栈顶元素了,则停止 pop + if count[stk[len(stk)-1]] == 0 { + break + } + // 若之后还有,则可以 pop + inStack[stk[len(stk)-1]] = false + stk = stk[:len(stk)-1] + } + stk = append(stk, c) + inStack[c] = true + } + + sb := strings.Builder{} + for i := len(stk) - 1; i >= 0; i-- { + sb.WriteRune(stk[i]) + } + return sb.String() +} +``` + +```java +// by labuladong (java) +class Solution { + public String removeDuplicateLetters(String s) { + Stack 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(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @return {string} + */ +var removeDuplicateLetters = function(s) { + let stk = []; + + // 维护一个计数器记录字符串中字符的数量 + // 因为输入为 ASCII 字符,大小 256 够用了 + let count = Array(256).fill(0); + for (let i = 0; i < s.length; i++) { + count[s.charCodeAt(i)]++; + } + + let inStack = Array(256).fill(false); + for (let i = 0; i < s.length; i++) { + let c = s.charAt(i); + // 每遍历过一个字符,都将对应的计数减一 + count[c.charCodeAt(0)]--; + + if (inStack[c.charCodeAt(0)]) continue; + + while (stk.length > 0 && stk[stk.length - 1] > c) { + // 若之后不存在栈顶元素了,则停止 pop + if (count[stk[stk.length - 1].charCodeAt(0)] == 0) { + break; + } + // 若之后还有,则可以 pop + inStack[stk.pop().charCodeAt(0)] = false; + } + stk.push(c); + inStack[c.charCodeAt(0)] = true; + } + + let sb = ""; + while (stk.length > 0) { + sb += stk.pop(); + } + return sb.split("").reverse().join(""); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def removeDuplicateLetters(self, s: str) -> str: + stk = [] + + # 维护一个计数器记录字符串中字符的数量 + # 因为输入为 ASCII 字符,大小 256 够用了 + count = [0] * 256 + for i in range(len(s)): + count[ord(s[i])] += 1 + + inStack = [False] * 256 + for c in s: + # 每遍历过一个字符,都将对应的计数减一 + count[ord(c)] -= 1 + + if inStack[ord(c)]: + continue + + while len(stk) > 0 and stk[-1] > c: + # 若之后不存在栈顶元素了,则停止 pop + if count[ord(stk[-1])] == 0: + break + # 若之后还有,则可以 pop + inStack[ord(stk.pop())] = False + stk.append(c) + inStack[ord(c)] = True + + sb = [] + while len(stk) > 0: + sb.append(stk.pop()) + return "".join(sb)[::-1] +``` + +https://leetcode.cn/problems/remove-duplicate-letters 的多语言解法👆 + +https://leetcode.cn/problems/remove-duplicates-from-sorted-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int removeDuplicates(vector& nums) { + if (nums.size() == 0) { + return 0; + } + int slow = 0, fast = 0; + while (fast < nums.size()) { + if (nums[fast] != nums[slow]) { + slow++; + // 维护 nums[0..slow] 无重复 + nums[slow] = nums[fast]; + } + fast++; + } + // 数组长度为索引 + 1 + return slow + 1; + } +}; +``` + +```go +// by chatGPT (go) +func removeDuplicates(nums []int) int { + // 如果数组为空,直接返回 0 + if len(nums) == 0 { + return 0 + } + // 定义快慢指针,初始化都指向数组头部 + slow, fast := 0, 0 + // 快指针向后遍历数组,直到末尾 + for fast < len(nums) { + // 如果两个指针指向的元素不相同 + if nums[fast] != nums[slow] { + // 慢指针向后移动,并且将慢指针位置上的值设为快指针位置上的值 + slow++ + nums[slow] = nums[fast] + } + // 快指针继续向后移动 + fast++ + } + // slow 指向数组的最后一个不重复元素的位置 + // 数组长度为索引 + 1 + return slow + 1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int removeDuplicates(int[] nums) { + if (nums.length == 0) { + return 0; + } + int slow = 0, fast = 0; + while (fast < nums.length) { + if (nums[fast] != nums[slow]) { + slow++; + // 维护 nums[0..slow] 无重复 + nums[slow] = nums[fast]; + } + fast++; + } + // 数组长度为索引 + 1 + return slow + 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number} + */ +var removeDuplicates = function(nums) { + if (nums.length === 0) { + return 0; + } + let slow = 0, fast = 0; + while (fast < nums.length) { + if (nums[fast] !== nums[slow]) { + slow++; + // 维护 nums[0..slow] 无重复 + nums[slow] = nums[fast]; + } + fast++; + } + // 数组长度为索引 + 1 + return slow + 1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def removeDuplicates(self, nums: List[int]) -> int: + if len(nums) == 0: + return 0 + slow, fast = 0, 0 + while fast < len(nums): + if nums[fast] != nums[slow]: + slow += 1 + # 维护 nums[0..slow] 无重复 + nums[slow] = nums[fast] + fast += 1 + # 数组长度为索引 + 1 + return slow + 1 +``` + +https://leetcode.cn/problems/remove-duplicates-from-sorted-array 的多语言解法👆 + +https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int removeDuplicates(vector& nums) { + if (nums.size() == 0) { + return 0; + } + // 快慢指针,维护 nums[0..slow] 为结果子数组 + int slow = 0, fast = 0; + // 记录一个元素重复的次数 + int count = 0; + while (fast < nums.size()) { + if (nums[fast] != nums[slow]) { + slow++; + nums[slow] = nums[fast]; + } else if (slow < fast && count < 2) { + // 当一个元素重复次数不到 2 次时,也 + slow++; + nums[slow] = nums[fast]; + } + fast++; + count++; + if (fast < nums.size() && nums[fast] != nums[fast - 1]) { + // 遇到不同的元素 + count = 0; + } + } + // 数组长度为索引 + 1 + return slow + 1; + } +}; +``` + +```go +// by chatGPT (go) +func removeDuplicates(nums []int) int { + if len(nums) == 0 { + return 0 + } + // 快慢指针,维护 nums[0..slow] 为结果子数组 + slow, fast := 0, 0 + // 记录一个元素重复的次数 + count := 0 + for fast < len(nums) { + if nums[fast] != nums[slow] { + slow++ + nums[slow] = nums[fast] + } else if slow < fast && count < 2 { + // 当一个元素重复次数不到 2 次时,也 + slow++ + nums[slow] = nums[fast] + } + fast++ + count++ + if fast < len(nums) && nums[fast] != nums[fast - 1] { + // 遇到不同的元素 + count = 0 + } + } + // 数组长度为索引 + 1 + return slow + 1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int removeDuplicates(int[] nums) { + if (nums.length == 0) { + return 0; + } + // 快慢指针,维护 nums[0..slow] 为结果子数组 + int slow = 0, fast = 0; + // 记录一个元素重复的次数 + int count = 0; + while (fast < nums.length) { + if (nums[fast] != nums[slow]) { + // 此时,对于 nums[0..slow] 来说,nums[fast] 是一个新的元素,加进来 + slow++; + nums[slow] = nums[fast]; + } else if (slow < fast && count < 2) { + // 此时,对于 nums[0..slow] 来说,nums[fast] 重复次数小于 2,也加进来 + slow++; + nums[slow] = nums[fast]; + } + fast++; + count++; + if (fast < nums.length && nums[fast] != nums[fast - 1]) { + // fast 遇到新的不同的元素时,重置 count + count = 0; + } + } + // 数组长度为索引 + 1 + return slow + 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var removeDuplicates = function(nums) { + if (nums.length == 0) { + return 0; + } + // 快慢指针,维护 nums[0..slow] 为结果子数组 + let slow = 0, fast = 0; + // 记录一个元素重复的次数 + let count = 0; + while (fast < nums.length) { + if (nums[fast] != nums[slow]) { + slow++; + nums[slow] = nums[fast]; + } else if (slow < fast && count < 2) { + // 当一个元素重复次数不到 2 次时,也 + slow++; + nums[slow] = nums[fast]; + } + fast++; + count++; + if (fast < nums.length && nums[fast] != nums[fast - 1]) { + // 遇到不同的元素 + count = 0; + } + } + // 数组长度为索引 + 1 + return slow + 1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def removeDuplicates(self, nums: List[int]) -> int: + if len(nums) == 0: + return 0 + # 快慢指针,维护 nums[0..slow] 为结果子数组 + slow, fast = 0, 0 + # 记录一个元素重复的次数 + count = 0 + while fast < len(nums): + if nums[fast] != nums[slow]: + slow += 1 + nums[slow] = nums[fast] + elif slow < fast and count < 2: + # 当一个元素重复次数不到 2 次时,也 + slow += 1 + nums[slow] = nums[fast] + fast += 1 + count += 1 + if fast < len(nums) and nums[fast] != nums[fast - 1]: + # 遇到不同的元素 + count = 0 + # 数组长度为索引 + 1 + return slow + 1 +``` + +https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii 的多语言解法👆 + +https://leetcode.cn/problems/remove-duplicates-from-sorted-list 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* deleteDuplicates(ListNode* head) { // 删除链表中重复的元素 + if (head == NULL) return NULL; // 如果链表为空,直接返回NULL + ListNode *slow = head, *fast = head; // 定义快慢指针,初始都指向头结点 + while (fast != NULL) { // 只要快指针没有遍历完整个链表 + if (fast->val != slow->val) { // 快慢指针值不同 + slow->next = fast; // 慢指针连接新节点 + slow = slow->next; // 慢指针向后移动一位 + } + fast = fast->next; // 快指针向后移动一位 + } + slow->next = NULL; // 断开与后面重复元素的连接 + return head; // 返回头结点 + } +}; +``` + +```go +// by chatGPT (go) +func deleteDuplicates(head *ListNode) *ListNode { + if head == nil { + return nil + } + + slow, fast := head, head + for fast != nil { + if fast.Val != slow.Val { + // nums[slow] = nums[fast]; + slow.Next = fast + // slow++; + slow = slow.Next + } + // fast++ + fast = fast.Next + } + // 断开与后面重复元素的连接 + slow.Next = nil + return head +} +``` + +```java +// by labuladong (java) +class Solution { + public deleteDuplicates(ListNode head) { + if (head == null) return null; + ListNode slow = head, fast = head; + while (fast != null) { + if (fast.val != slow.val) { + // nums[slow] = nums[fast]; + slow.next = fast; + // slow++; + slow = slow.next; + } + // fast++ + fast = fast.next; + } + // 断开与后面重复元素的连接 + slow.next = null; + return head; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} head + * @return {ListNode} + */ +var deleteDuplicates = function(head) { + if (head === null) return null; + var slow = head; + var fast = head; + while (fast !== null) { + if (fast.val !== slow.val) { + // nums[slow] = nums[fast]; + slow.next = fast; + // slow++; + slow = slow.next; + } + // fast++ + fast = fast.next; + } + // 断开与后面重复元素的连接 + slow.next = null; + return head; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def deleteDuplicates(self, head: ListNode) -> ListNode: + if head == None: + return None + slow = head + fast = head + while fast != None: + if fast.val != slow.val: + # nums[slow] = nums[fast]; + slow.next = fast + # slow++; + slow = slow.next + # fast++ + fast = fast.next + # 断开与后面重复元素的连接 + slow.next = None + return head +``` + +https://leetcode.cn/problems/remove-duplicates-from-sorted-list 的多语言解法👆 + +https://leetcode.cn/problems/remove-element 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int removeElement(vector& nums, int val) { + int fast = 0, slow = 0; + while (fast < nums.size()) { + if (nums[fast] != val) { + nums[slow] = nums[fast]; + slow++; + } + fast++; + } + return slow; + } +}; +``` + +```go +// by chatGPT (go) +// 双指针法 +func removeElement(nums []int, val int) int { + var fast, slow int + for fast < len(nums) { + if nums[fast] != val { + nums[slow] = nums[fast] + slow++ + } + fast++ + } + return slow +} +``` + +```java +// by labuladong (java) +class Solution { + public int removeElement(int[] nums, int val) { + int fast = 0, slow = 0; + while (fast < nums.length) { + if (nums[fast] != val) { + nums[slow] = nums[fast]; + slow++; + } + fast++; + } + return slow; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @param {number} val + * @return {number} + */ +var removeElement = function(nums, val) { + let fast = 0, slow = 0; + while (fast < nums.length) { + if (nums[fast] !== val) { + nums[slow] = nums[fast]; + slow++; + } + fast++; + } + return slow; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + fast, slow = 0, 0 + while fast < len(nums): + if nums[fast] != val: + nums[slow] = nums[fast] + slow += 1 + fast += 1 + return slow +``` + +https://leetcode.cn/problems/remove-element 的多语言解法👆 + +https://leetcode.cn/problems/remove-nth-node-from-end-of-list 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* removeNthFromEnd(ListNode* head, int n) { + // 虚拟头结点 + ListNode* dummy = new ListNode(-1); + dummy->next = head; + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + ListNode* x = findFromEnd(dummy, n + 1); + // 删掉倒数第 n 个节点 + x->next = x->next->next; + return dummy->next; + } + + // 返回链表的倒数第 k 个节点 + ListNode* findFromEnd(ListNode* head, int k) { + ListNode* p1 = head; + // p1 先走 k 步 + for (int i = 0; i < k; i++) { + p1 = p1->next; + } + ListNode* p2 = head; + // p1 和 p2 同时走 n - k 步 + while (p1 != NULL) { + p2 = p2->next; + p1 = p1->next; + } + // p2 现在指向第 n - k 个节点 + return p2; + } +}; +``` + +```go +// by chatGPT (go) +// 主函数 +func removeNthFromEnd(head *ListNode, n int) *ListNode { + // 虚拟头结点 + dummy := &ListNode{Val: -1, Next: head} + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + x := findFromEnd(dummy, n + 1) + // 删掉倒数第 n 个节点 + x.Next = x.Next.Next + return dummy.Next +} + +// 返回链表的倒数第 k 个节点 +func findFromEnd(head *ListNode, k int) *ListNode { + p1, p2 := head, head + // p1 先走 k 步 + for i := 0; i < k; i++ { + p1 = p1.Next + } + // p1 和 p2 同时走 n - k 步 + for p1 != nil { + p1 = p1.Next + p2 = p2.Next + } + // p2 现在指向第 n - k 个节点 + return p2 +} +``` + +```java +// by labuladong (java) +class Solution { + // 主函数 + public ListNode removeNthFromEnd(ListNode head, int n) { + // 虚拟头结点 + ListNode dummy = new ListNode(-1); + dummy.next = head; + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + ListNode x = findFromEnd(dummy, n + 1); + // 删掉倒数第 n 个节点 + x.next = x.next.next; + return dummy.next; + } + + // 返回链表的倒数第 k 个节点 + ListNode findFromEnd(ListNode head, int k) { + ListNode p1 = head; + // p1 先走 k 步 + for (int i = 0; i < k; i++) { + p1 = p1.next; + } + ListNode p2 = head; + // p1 和 p2 同时走 n - k 步 + while (p1 != null) { + p2 = p2.next; + p1 = p1.next; + } + // p2 现在指向第 n - k 个节点 + return p2; + } +} +``` + +```javascript +// by chatGPT (javascript) +var removeNthFromEnd = function(head, n) { + // 虚拟头结点 + let dummy = new ListNode(-1); + dummy.next = head; + // 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + let x = findFromEnd(dummy, n + 1); + // 删掉倒数第 n 个节点 + x.next = x.next.next; + return dummy.next; +}; + +// 返回链表的倒数第 k 个节点 +var findFromEnd = function(head, k) { + let p1 = head; + // p1 先走 k 步 + for (let i = 0; i < k; i++) { + p1 = p1.next; + } + let p2 = head; + // p1 和 p2 同时走 n - k 步 + while (p1 != null) { + p2 = p2.next; + p1 = p1.next; + } + // p2 现在指向第 n - k 个节点 + return p2; +}; +``` + +```python +# by chatGPT (python) +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next + +class Solution: + def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: + # 虚拟头结点 + dummy = ListNode(-1) + dummy.next = head + # 删除倒数第 n 个,要先找倒数第 n + 1 个节点 + x = self.findFromEnd(dummy, n + 1) + # 删掉倒数第 n 个节点 + x.next = x.next.next + return dummy.next + + # 返回链表的倒数第 k 个节点 + def findFromEnd(self, head: ListNode, k: int) -> ListNode: + p1 = head + # p1 先走 k 步 + for i in range(k): + p1 = p1.next + p2 = head + # p1 和 p2 同时走 n - k 步 + while p1 != None: + p1 = p1.next + p2 = p2.next + # p2 现在指向第 n - k 个节点 + return p2 +``` + +https://leetcode.cn/problems/remove-nth-node-from-end-of-list 的多语言解法👆 + +https://leetcode.cn/problems/reshape-the-matrix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> matrixReshape(vector>& mat, int r, int c) { + int m = mat.size(), n = mat[0].size(); + // 如果想成功 reshape,元素个数应该相同 + if (r * c != m * n) { + return mat; + } + + vector> res(r, vector(c, 0)); + for (int i = 0; i < m * n; i++) { + set(res, i, get(mat, i)); + } + return res; + } + + // 通过一维坐标访问二维数组中的元素 + int get(vector>& matrix, int index) { + int m = matrix.size(), n = matrix[0].size(); + // 计算二维中的横纵坐标 + int i = index / n, j = index % n; + return matrix[i][j]; + } + + // 通过一维坐标设置二维数组中的元素 + void set(vector>& matrix, int index, int value) { + int m = matrix.size(), n = matrix[0].size(); + // 计算二维中的横纵坐标 + int i = index / n, j = index % n; + matrix[i][j] = value; + } +}; +``` + +```go +// by chatGPT (go) +func matrixReshape(mat [][]int, r int, c int) [][]int { + m := len(mat) + n := len(mat[0]) + + if r*c != m*n { + return mat + } + + res := make([][]int,r) + for i := range res { + res[i] = make([]int,c) + } + + for i := 0; i < m * n; i++ { + set(res, i, get(mat, i)) + } + return res +} + +// get 通过一维坐标访问二维数组中的元素 +func get(matrix [][]int, index int) int { + n := len(matrix[0]) + i := index / n + j := index % n + return matrix[i][j] +} + +// set 通过一维坐标设置二维数组中的元素 +func set(matrix [][]int, index int, value int) { + n := len(matrix[0]) + i := index / n + j := index % n + matrix[i][j] = value +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] matrixReshape(int[][] mat, int r, int c) { + int m = mat.length, n = mat[0].length; + // 如果想成功 reshape,元素个数应该相同 + if (r * c != m * n) { + return mat; + } + + int[][] res = new int[r][c]; + for (int i = 0; i < m * n; i++) { + set(res, i, get(mat, i)); + } + return res; + } + + // 通过一维坐标访问二维数组中的元素 + int get(int[][] matrix, int index) { + int m = matrix.length, n = matrix[0].length; + // 计算二维中的横纵坐标 + int i = index / n, j = index % n; + return matrix[i][j]; + } + + // 通过一维坐标设置二维数组中的元素 + void set(int[][] matrix, int index, int value) { + int m = matrix.length, n = matrix[0].length; + // 计算二维中的横纵坐标 + int i = index / n, j = index % n; + matrix[i][j] = value; + } +} +``` + +```javascript +// by chatGPT (javascript) +var matrixReshape = function(mat, r, c) { + var m = mat.length, n = mat[0].length; + // 如果想成功 reshape,元素个数应该相同 + if (r * c !== m * n) { + return mat; + } + + var res = new Array(r); + for (var i = 0; i < r; i++) { + res[i] = new Array(c); + } + + for (var i = 0; i < m * n; i++) { + set(res, i, get(mat, i)); + } + return res; +} + +// 通过一维坐标访问二维数组中的元素 +var get = function(matrix, index) { + var m = matrix.length, n = matrix[0].length; + // 计算二维中的横纵坐标 + var i = Math.floor(index / n), j = index % n; + return matrix[i][j]; +} + +// 通过一维坐标设置二维数组中的元素 +var set = function(matrix, index, value) { + var m = matrix.length, n = matrix[0].length; + // 计算二维中的横纵坐标 + var i = Math.floor(index / n), j = index % n; + matrix[i][j] = value; +} +``` + +```python +# by chatGPT (python) +class Solution: + def matrixReshape(self, mat: List[List[int]], r: int, c: int) -> List[List[int]]: + m, n = len(mat), len(mat[0]) + # 如果想成功 reshape,元素个数应该相同 + if r * c != m * n: + return mat + + res = [[0] * c for _ in range(r)] + for i in range(m * n): + self.set(res, i, self.get(mat, i)) + return res + + # 通过一维坐标访问二维数组中的元素 + def get(self, matrix:List[List[int]], index:int) -> int: + m, n = len(matrix), len(matrix[0]) + # 计算二维中的横纵坐标 + i, j = index // n, index % n + return matrix[i][j] + + # 通过一维坐标设置二维数组中的元素 + def set(self, matrix:List[List[int]], index:int, value:int) -> None: + m, n = len(matrix), len(matrix[0]) + # 计算二维中的横纵坐标 + i, j = index // n, index % n + matrix[i][j] = value +``` + +https://leetcode.cn/problems/reshape-the-matrix 的多语言解法👆 + +https://leetcode.cn/problems/reverse-linked-list 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * ListNode *next; + * ListNode(int x) : val(x), next(NULL) {} + * }; + */ +class Solution { +public: + ListNode* reverseList(ListNode* head) { + if (head == NULL || head->next == NULL) { + return head; + } + ListNode* last = reverseList(head->next); + /** + ![](../pictures/反转链表/3.jpg) + */ + head->next->next = head; + /** + ![](../pictures/反转链表/4.jpg) + */ + head->next = NULL; + /** + ![](../pictures/反转链表/5.jpg) + */ + return last; + } +}; +``` + +```go +// by chatGPT (go) +func reverseList(head *ListNode) *ListNode { + if head == nil || head.Next == nil { + return head + } + last := reverseList(head.Next) + /* + ![](../pictures/反转链表/3.jpg) + */ + head.Next.Next = head + /* + ![](../pictures/反转链表/4.jpg) + */ + head.Next = nil + /* + ![](../pictures/反转链表/5.jpg) + */ + return last +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode last = reverseList(head.next); + /** + ![](../pictures/反转链表/3.jpg) + */ + head.next.next = head; + /** + ![](../pictures/反转链表/4.jpg) + */ + head.next = null; + /** + ![](../pictures/反转链表/5.jpg) + */ + return last; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * Definition for singly-linked list. + * function ListNode(val, next) { + * this.val = (val===undefined ? 0 : val) + * this.next = (next===undefined ? null : next) + * } + */ +/** + * @param {ListNode} head + * @return {ListNode} + */ +var reverseList = function(head) { + if (head === null || head.next === null) { + return head; + } + var last = reverseList(head.next); + /** + ![](../pictures/反转链表/3.jpg) + */ + head.next.next = head; + /** + ![](../pictures/反转链表/4.jpg) + */ + head.next = null; + /** + ![](../pictures/反转链表/5.jpg) + */ + return last; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def reverseList(self, head: ListNode) -> ListNode: + if head is None or head.next is None: + return head + last = self.reverseList(head.next) + """ + + ![](../pictures/反转链表/3.jpg) + """ + head.next.next = head + """ + + ![](../pictures/反转链表/4.jpg) + """ + head.next = None + """ + + ![](../pictures/反转链表/5.jpg) + """ + return last +``` + +https://leetcode.cn/problems/reverse-linked-list 的多语言解法👆 + +https://leetcode.cn/problems/reverse-linked-list-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* reverseBetween(ListNode* head, int m, int n) { + // base case + if (m == 1) { + return reverseN(head, n); + } + // 前进到反转的起点触发 base case + head->next = reverseBetween(head->next, m - 1, n - 1); + return head; + } + +private: + ListNode* successor = nullptr; // 后驱节点 + // 反转以 head 为起点的 n 个节点,返回新的头结点 + ListNode* reverseN(ListNode* head, int n) { + if (n == 1) { + // 记录第 n + 1 个节点 + successor = head->next; + return head; + } + // 以 head->next 为起点,需要反转前 n - 1 个节点 + ListNode* last = reverseN(head->next, n - 1); + + head->next->next = head; + // 让反转之后的 head 节点和后面的节点连起来 + head->next = successor; + return last; + /** + ![](../pictures/反转链表/7.jpg) + */ + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ + +func reverseBetween(head *ListNode, m int, n int) *ListNode { + var successor *ListNode // 后驱节点 + + // 反转以 head 为起点的 n 个节点,返回新的头结点 + var reverseN func(head *ListNode, n int) (*ListNode) + reverseN = func(head *ListNode, n int) (*ListNode) { + if n == 1 { + // 记录第 n + 1 个节点 + successor = head.Next + return head + } + // 以 head.Next 为起点,需要反转前 n - 1 个节点 + last := reverseN(head.Next, n - 1) + + head.Next.Next = head + // 让反转之后的 head 节点和后面的节点连起来 + head.Next = successor + return last + } + + // base case + if m == 1 { + return reverseN(head, n) + } + // 前进到反转的起点触发 base case + head.Next = reverseBetween(head.Next, m - 1, n - 1) + return head +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + // base case + if (m == 1) { + return reverseN(head, n); + } + // 前进到反转的起点触发 base case + head.next = reverseBetween(head.next, m - 1, n - 1); + return head; + } + + ListNode successor = null; // 后驱节点 + // 反转以 head 为起点的 n 个节点,返回新的头结点 + ListNode reverseN(ListNode head, int n) { + if (n == 1) { + // 记录第 n + 1 个节点 + successor = head.next; + return head; + } + // 以 head.next 为起点,需要反转前 n - 1 个节点 + ListNode last = reverseN(head.next, n - 1); + + head.next.next = head; + // 让反转之后的 head 节点和后面的节点连起来 + head.next = successor; + return last; + /** + ![](../pictures/反转链表/7.jpg) + */ + } +} +``` + +```javascript +// by chatGPT (javascript) +var reverseBetween = function(head, m, n) { + let successor = null; + // 反转以 head 为起点的 n 个节点,返回新的头结点 + const reverseN = function(head, n) { + if (n == 1) { + // 记录第 n + 1 个节点 + successor = head.next; + return head; + } + const last = reverseN(head.next, n - 1); + head.next.next = head; + // 让反转之后的 head 节点和后面的节点连起来 + head.next = successor; + return last; + /** + ![](../pictures/反转链表/7.jpg) + */ + }; + // base case + if (m == 1) { + return reverseN(head, n); + } + // 前进到反转的起点触发 base case + head.next = reverseBetween(head.next, m - 1, n - 1); + return head; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode: + # base case + if m == 1: + return self.reverseN(head, n) + # 前进到反转的起点触发 base case + head.next = self.reverseBetween(head.next, m - 1, n - 1) + return head + + successor = None # 后驱节点 + # 反转以 head 为起点的 n 个节点,返回新的头结点 + def reverseN(self, head: ListNode, n: int) -> ListNode: + if n == 1: + # 记录第 n + 1 个节点 + self.successor = head.next + return head + # 以 head.next 为起点,需要反转前 n - 1 个节点 + last = self.reverseN(head.next, n - 1) + + head.next.next = head + # 让反转之后的 head 节点和后面的节点连起来 + head.next = self.successor + return last + """ + + ![](../pictures/反转链表/7.jpg) + """ +``` + +https://leetcode.cn/problems/reverse-linked-list-ii 的多语言解法👆 + +https://leetcode.cn/problems/reverse-nodes-in-k-group 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* reverseKGroup(ListNode* head, int k) { + if(head == NULL) return NULL; + //区间[a,b)包含k个待反转元素 + ListNode *a, *b; + a = b = head; + for (int i = 0; i < k; i++) { + //不足k个,直接返回 + if (b == NULL) return head; + b = b->next; + } + //反转前k个元素 + ListNode *newHead = reverse(a, b); + //递归反转后续链表并连接起来 + a->next = reverseKGroup(b, k); + return newHead; + } + ListNode *reverse(ListNode *a, ListNode *b) { + ListNode *pre, *cur, *nxt; + cur = a; pre = NULL; + while (cur != b) { + nxt = cur->next; + cur->next = pre; + pre = cur; + cur = nxt; + } + return pre; + } +}; +``` + +```go +// by chatGPT (go) +// 给出一个链表和一个数k,比如1->2->3->4->5->6,k=2,那么你需要返回的结果是2->1->4->3->6->5。 +func reverseKGroup(head *ListNode, k int) *ListNode { + if head == nil { + return nil + } + + a, b := head, head + // 找到需要翻转的区间 [a, b) + for i := 0; i < k; i++ { + if b == nil { + return head + } + b = b.Next + } + + // 反转区间内的链表 + newHead := reverse(a, b) + // 递归将后续链表的区间也翻转,然后再将它链接到新的区间内 + a.Next = reverseKGroup(b, k) + /** + ![](../pictures/kgroup/6.jpg) + */ + + return newHead +} + +// 翻转区间 [a, b),包头不包尾 +func reverse(a, b *ListNode) *ListNode { + // 初始化三个指针 + pre, cur, nxt := (*ListNode)(nil), a, a + + // 循环将当前节点指向前一个节点,然后将前一个节点和当前节点往后移动 + for cur != b { + nxt = cur.Next + cur.Next = pre + pre, cur = cur, nxt + } + + // 返回新的头结点 + return pre +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode reverseKGroup(ListNode head, int k) { + if (head == null) return null; + // 区间 [a, b) 包含 k 个待反转元素 + ListNode a, b; + a = b = head; + for (int i = 0; i < k; i++) { + // 不足 k 个,不需要反转,base case + if (b == null) return head; + b = b.next; + } + // 反转前 k 个元素 + ListNode newHead = reverse(a, b); + // 递归反转后续链表并连接起来 + a.next = reverseKGroup(b, k); + /** + ![](../pictures/kgroup/6.jpg) + */ + return newHead; + } + + /* 反转区间 [a, b) 的元素,注意是左闭右开 */ + ListNode reverse(ListNode a, ListNode b) { + /** + ![](../pictures/kgroup/8.gif) + */ + ListNode pre, cur, nxt; + pre = null; + cur = a; + nxt = a; + // while 终止的条件改一下就行了 + while (cur != b) { + nxt = cur.next; + cur.next = pre; + pre = cur; + cur = nxt; + } + // 返回反转后的头结点 + return pre; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} head + * @param {number} k + * @return {ListNode} + */ +var reverseKGroup = function(head, k) { + if (!head) return null; + // 区间 [a, b) 包含 k 个待反转元素 + let a = head, b = head; + for (let i = 0; i < k; i++) { + // 不足 k 个,不需要反转,base case + if (!b) return head; + b = b.next; + } + // 反转前 k 个元素 + let newHead = reverse(a, b); + // 递归反转后续链表并连接起来 + a.next = reverseKGroup(b, k); + /** + ![](../pictures/kgroup/6.jpg) + */ + return newHead; +}; + +/* 反转区间 [a, b) 的元素,注意是左闭右开 */ +var reverse = function(a, b) { + /** + ![](../pictures/kgroup/8.gif) + */ + let pre = null, cur = a, nxt = a; + // while 终止的条件改一下就行了 + while (cur !== b) { + nxt = cur.next; + cur.next = pre; + pre = cur; + cur = nxt; + } + // 返回反转后的头结点 + return pre; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def reverseKGroup(self, head: ListNode, k: int) -> ListNode: + if not head: + return None + # 区间 [a, b) 包含 k 个待反转元素 + a = b = head + for i in range(k): + # 不足 k 个,不需要反转,base case + if not b: + return head + b = b.next + # 反转前 k 个元素 + newHead = self.reverse(a, b) + # 递归反转后续链表并连接起来 + a.next = self.reverseKGroup(b, k) + """ + + ![](../pictures/kgroup/6.jpg) + """ + return newHead + + """ 反转区间 [a, b) 的元素,注意是左闭右开 """ + def reverse(self, a: ListNode, b: ListNode) -> ListNode: + """ + + ![](../pictures/kgroup/8.gif) + """ + pre, cur, nxt = None, a, a + # while 终止的条件改一下就行了 + while cur != b: + nxt = cur.next + cur.next = pre + pre = cur + cur = nxt + # 返回反转后的头结点 + return pre +``` + +https://leetcode.cn/problems/reverse-nodes-in-k-group 的多语言解法👆 + +https://leetcode.cn/problems/reverse-words-in-a-string 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string reverseWords(string s) { + string res; + // 清洗一下数据,去除多余的空格 + for (int i = 0; i < s.size(); i++) { + char c = s[i]; + if (c != ' ') { + // 单词中的字母/数字 + res += c; + } + + else if (res.back() != ' ') { + // 单词之间保留一个空格 + res += ' '; + } + } + // 末尾如果有空格,清除之 + if (res.back() == ' ') { + res.pop_back(); + } + + // 清洗之后的字符串 + char *chars = new char[res.size() + 1]; + strcpy(chars, res.c_str()); + int n = strlen(chars); + // 进行单词的翻转,先整体翻转 + reverse(chars, 0, n - 1); + // 再把每个单词翻转 + for (int i = 0; i < n; ) { + for (int j = i; j < n; j++) { + if (j + 1 == n || chars[j + 1] == ' ') { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j); + // 把 i 置为下一个单词的首字母 + i = j + 2; + break; + } + } + } + // 最后得到题目想要的结果 + return chars; + } + + // 翻转 arr[i..j] + void reverse(char* arr, int i, int j) { + while (i < j) { + char temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +}; +``` + +```go +// by chatGPT (go) +func reverseWords(s string) string { + sb := strings.Builder{} + // 先清洗一下数据,把多于的空格都删掉 + for i := 0; i < len(s); i++ { + c := s[i] + if c != ' ' { + // 单词中的字母/数字 + sb.WriteByte(c) + } else if sb.Len() > 0 && sb.String()[sb.Len()-1] != ' ' { + // 单词之间保留一个空格 + sb.WriteByte(' ') + } + } + // 末尾如果有空格,清除之 + str := sb.String() + if len(str) > 0 && str[len(str)-1] == ' ' { + str = str[:len(str)-1] + } + + // 清洗之后的字符串 + chars := []byte(str) + n := len(chars) + // 进行单词的翻转,先整体翻转 + reverse(chars, 0, n-1) + // 再把每个单词翻转 + for i := 0; i < n; { + for j := i; j < n; j++ { + if j+1 == n || chars[j+1] == ' ' { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j) + // 把 i 置为下一个单词的首字母 + i = j + 2 + break + } + } + } + // 最后得到题目想要的结果 + return string(chars) +} + +// 翻转 arr[i..j] +func reverse(arr []byte, i, j int) { + for i < j { + arr[i], arr[j] = arr[j], arr[i] + i++ + j-- + } +} +``` + +```java +// by labuladong (java) +class Solution { + public String reverseWords(String s) { + StringBuilder sb = new StringBuilder(); + // 先清洗一下数据,把多于的空格都删掉 + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c != ' ') { + // 单词中的字母/数字 + sb.append(c); + } else if (!sb.isEmpty() && sb.charAt(sb.length() - 1) != ' ') { + // 单词之间保留一个空格 + sb.append(' '); + } + } + if (sb.isEmpty()) { + return ""; + } + // 末尾如果有空格,清除之 + if (sb.charAt(sb.length() - 1) == ' ') { + sb.deleteCharAt(sb.length() - 1); + } + + // 清洗之后的字符串 + char[] chars = sb.toString().toCharArray(); + int n = chars.length; + // 进行单词的翻转,先整体翻转 + reverse(chars, 0, n - 1); + // 再把每个单词翻转 + for (int i = 0; i < n; ) { + for (int j = i; j < n; j++) { + if (j + 1 == n || chars[j + 1] == ' ') { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j); + // 把 i 置为下一个单词的首字母 + i = j + 2; + break; + } + } + } + // 最后得到题目想要的结果 + return new String(chars); + } + + // 翻转 arr[i..j] + void reverse(char[] arr, int i, int j) { + while (i < j) { + char temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var reverseWords = function(s) { + let sb = ""; + // 先清洗一下数据,把多于的空格都删掉 + for (let i = 0; i < s.length; i++) { + let c = s.charAt(i); + if (c !== ' ') { + // 单词中的字母/数字 + sb += c; + } else if (sb !== "" && sb[sb.length - 1] !== ' ') { + // 单词之间保留一个空格 + sb += ' '; + } + } + // 末尾如果有空格,清除之 + if (sb[sb.length - 1] === ' ') { + sb = sb.substring(0, sb.length - 1); + } + + // 清洗之后的字符串 + let chars = sb.split(''); + let n = chars.length; + // 进行单词的翻转,先整体翻转 + reverse(chars, 0, n - 1); + // 再把每个单词翻转 + for (let i = 0; i < n;) { + for (let j = i; j < n; j++) { + if (j + 1 === n || chars[j + 1] === ' ') { + // chars[i..j] 是一个单词,翻转之 + reverse(chars, i, j); + // 把 i 置为下一个单词的首字母 + i = j + 2; + break; + } + } + } + // 最后得到题目想要的结果 + return chars.join(""); +}; + +// 翻转 arr[i..j] +function reverse(arr, i, j) { + while (i < j) { + let temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def reverseWords(self, s: str) -> str: + sb = [] + # 先清洗一下数据,把多余的空格都删掉 + for c in s: + if c != ' ': + # 单词中的字母/数字 + sb.append(c) + elif sb and sb[-1] != ' ': + # 单词之间保留一个空格 + sb.append(' ') + # 末尾如果有空格,清除之 + if sb and sb[-1] == ' ': + sb.pop() + + # 清洗之后的字符串 + chars = sb + + # 进行单词的翻转,先整体翻转 + def reverse(l, r): + while l < r: + chars[l], chars[r] = chars[r], chars[l] + l += 1 + r -= 1 + + n = len(chars) + reverse(0, n-1) + + # 再把每个单词翻转 + start = end = 0 + while end < n: + if chars[end] == ' ': + reverse(start, end-1) + start = end + 1 + end += 1 + + # 翻转最后一个单词 + reverse(start, n-1) + + # 最后得到题目想要的结果 + return "".join(chars) +``` + +https://leetcode.cn/problems/reverse-words-in-a-string 的多语言解法👆 + +https://leetcode.cn/problems/rotate-image 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + void rotate(vector>& matrix) { + int n = matrix.size(); + // 先沿对角线反转二维矩阵 + for (int i = 0; i < n; i++) { + for (int j = i; j < n; j++) { + // swap(matrix[i][j], matrix[j][i]); + int temp = matrix[i][j]; + matrix[i][j] = matrix[j][i]; + matrix[j][i] = temp; + } + } + // 然后反转二维矩阵的每一行 + for (auto& row : matrix) { + ::reverse(row.begin(), row.end()); + } + } + + // 反转一维数组 + void reverse(vector& arr) { + int i = 0, j = arr.size() - 1; + while (j > i) { + // swap(arr[i], arr[j]); + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +}; +``` + +```go +// by chatGPT (go) +func rotate(matrix [][]int) { + n := len(matrix) + // 先沿对角线反转二维矩阵 + for i := 0; i < n; i++ { + for j := i; j < n; j++ { + matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] + } + } + // 然后反转二维矩阵的每一行 + for _, row := range matrix { + reverse(row) + } +} + +// 反转一维数组 +func reverse(arr []int) { + i, j := 0, len(arr) - 1 + for j > i { + arr[i], arr[j] = arr[j], arr[i] + i++ + j-- + } +} +``` + +```java +// by labuladong (java) +class Solution { + public void rotate(int[][] matrix) { + int n = matrix.length; + // 先沿对角线反转二维矩阵 + for (int i = 0; i < n; i++) { + for (int j = i; j < n; j++) { + // swap(matrix[i][j], matrix[j][i]); + int temp = matrix[i][j]; + matrix[i][j] = matrix[j][i]; + matrix[j][i] = temp; + } + } + // 然后反转二维矩阵的每一行 + for (int[] row : matrix) { + reverse(row); + } + } + + // 反转一维数组 + void reverse(int[] arr) { + int i = 0, j = arr.length - 1; + while (j > i) { + // swap(arr[i], arr[j]); + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} matrix + * @return {void} Do not return anything, modify matrix in-place instead. + */ +var rotate = function(matrix) { + var n = matrix.length; + // 先沿对角线反转二维矩阵 + for (var i = 0; i < n; i++) { + for (var j = i; j < n; j++) { + // swap(matrix[i][j], matrix[j][i]); + var temp = matrix[i][j]; + matrix[i][j] = matrix[j][i]; + matrix[j][i] = temp; + } + } + // 然后反转二维矩阵的每一行 + for (var row of matrix) { + reverse(row); + } +} + +// 反转一维数组 +function reverse(arr) { + var i = 0, j = arr.length - 1; + while (j > i) { + // swap(arr[i], arr[j]); + var temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def rotate(self, matrix: List[List[int]]) -> None: + n = len(matrix) + # 先沿对角线反转二维矩阵 + for i in range(n): + for j in range(i, n): + # swap(matrix[i][j], matrix[j][i]); + temp = matrix[i][j] + matrix[i][j] = matrix[j][i] + matrix[j][i] = temp + # 然后反转二维矩阵的每一行 + for row in matrix: + self.reverse(row) + + # 反转一维数组 + def reverse(self, arr: List[int]) -> None: + i, j = 0, len(arr) - 1 + while j > i: + # swap(arr[i], arr[j]); + temp = arr[i] + arr[i] = arr[j] + arr[j] = temp + i += 1 + j -= 1 +``` + +https://leetcode.cn/problems/rotate-image 的多语言解法👆 + +https://leetcode.cn/problems/russian-doll-envelopes 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int maxEnvelopes(vector>& envelopes) { + int n = envelopes.size(); + // 按宽度升序排列,如果宽度一样,则按高度降序排列 + sort(envelopes.begin(), envelopes.end(), [](vector& a, vector& b){ + if(a[0] == b[0]){ + return b[1] < a[1]; + }else{ + return a[0] < b[0]; + } + }); + // 对高度数组寻找 LIS + vector height(n); + for (int i = 0; i < n; i++){ + height[i] = envelopes[i][1]; + } + return lengthOfLIS(height); + } + + /* 返回 nums 中 LIS 的长度 */ + int lengthOfLIS(vector& nums) { + int piles = 0, n = nums.size(); + vector top(n); + for (int i = 0; i < n; i++) { + // 要处理的扑克牌 + int poker = nums[i]; + int left = 0, right = piles; + // 二分查找插入位置 + while (left < right) { + int mid = (left + right) / 2; + if (top[mid] >= poker) + right = mid; + else + left = mid + 1; + } + if (left == piles) piles++; + // 把这张牌放到牌堆顶 + top[left] = poker; + } + // 牌堆数就是 LIS 长度 + return piles; + } +}; +``` + +```go +// by chatGPT (go) +import "sort" + +func maxEnvelopes(envelopes [][]int) int { + n := len(envelopes) + // 按宽度升序排列,如果宽度一样,则按高度降序排列 + sort.Slice(envelopes, func(i, j int) bool { + if envelopes[i][0] < envelopes[j][0] { + return true + } + if envelopes[i][0] == envelopes[j][0] { + return envelopes[i][1] > envelopes[j][1] + } + return false + }) + // 对高度数组寻找 LIS + height := make([]int, n) + for i := 0; i < n; i++ { + height[i] = envelopes[i][1] + } + + return lengthOfLIS(height) +} + +/* 返回 nums 中 LIS 的长度 */ +func lengthOfLIS(nums []int) int { + piles, n := 0, len(nums) + top := make([]int, n) + for i := 0; i < n; i++ { + // 要处理的扑克牌 + poker := nums[i] + left, right := 0, piles + // 二分查找插入位置 + for left < right { + mid := (left + right) / 2 + if top[mid] >= poker { + right = mid + } else { + left = mid + 1 + } + } + if left == piles { + piles++ + } + // 把这张牌放到牌堆顶 + top[left] = poker + } + // 牌堆数就是 LIS 长度 + return piles +} +``` + +```java +// by labuladong (java) +class Solution { + public int maxEnvelopes(int[][] envelopes) { + int n = envelopes.length; + // 按宽度升序排列,如果宽度一样,则按高度降序排列 + Arrays.sort(envelopes, new Comparator() + { + public int compare(int[] a, int[] b) { + return a[0] == b[0] ? + b[1] - a[1] : a[0] - b[0]; + } + }); + // 对高度数组寻找 LIS + int[] height = new int[n]; + for (int i = 0; i < n; i++) + height[i] = envelopes[i][1]; + + return lengthOfLIS(height); + } + + /* 返回 nums 中 LIS 的长度 */ + public int lengthOfLIS(int[] nums) { + int piles = 0, n = nums.length; + int[] top = new int[n]; + for (int i = 0; i < n; i++) { + // 要处理的扑克牌 + int poker = nums[i]; + int left = 0, right = piles; + // 二分查找插入位置 + while (left < right) { + int mid = (left + right) / 2; + if (top[mid] >= poker) + right = mid; + else + left = mid + 1; + } + if (left == piles) piles++; + // 把这张牌放到牌堆顶 + top[left] = poker; + } + // 牌堆数就是 LIS 长度 + return piles; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} envelopes + * @return {number} + */ +var maxEnvelopes = function(envelopes) { + const n = envelopes.length; + // 按宽度升序排列,如果宽度一样,则按高度降序排列 + envelopes.sort((a, b) => { + if (a[0] === b[0]) { + return b[1] - a[1]; + } else { + return a[0] - b[0]; + } + }); + + // 对高度数组寻找 LIS + const height = new Array(n).fill(0).map((_, i) => envelopes[i][1]); + return lengthOfLIS(height); +} + +/** + * @param {number[]} nums + * @return {number} + */ +const lengthOfLIS = (nums) => { + // piles 表示牌堆数 + let piles = 0; + const n = nums.length; + const top = new Array(n).fill(0); + + for (let i = 0; i < n; i++) { + // 要处理的扑克牌 + let poker = nums[i]; + let left = 0; + let right = piles; + + // 二分查找插入位置 + while (left < right) { + let mid = Math.floor((left + right) / 2); + if (top[mid] >= poker) { + right = mid; + } else { + left = mid + 1; + } + } + + if (left === piles) piles++; + // 把这张牌放到牌堆顶 + top[left] = poker; + } + + // 牌堆数就是 LIS 长度 + return piles; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def maxEnvelopes(self, envelopes: List[List[int]]) -> int: + n = len(envelopes) + # 按宽度升序排列,如果宽度一样,则按高度降序排列 + envelopes.sort(key=lambda x: (x[0], -x[1])) + # 对高度数组寻找 LIS + height = [0] * n + for i in range(n): + height[i] = envelopes[i][1] + + return self.lengthOfLIS(height) + + def lengthOfLIS(self, nums: List[int]) -> int: + piles = 0 + n = len(nums) + top = [0] * n + for i in range(n): + # 要处理的扑克牌 + poker = nums[i] + left, right = 0, piles + # 二分查找插入位置 + while left < right: + mid = (left + right) // 2 + if top[mid] >= poker: + right = mid + else: + left = mid + 1 + if left == piles: + piles += 1 + # 把这张牌放到牌堆顶 + top[left] = poker + # 牌堆数就是 LIS 长度 + return piles +``` + +https://leetcode.cn/problems/russian-doll-envelopes 的多语言解法👆 + +https://leetcode.cn/problems/same-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 定义:输入两个根节点,返回以它们为根的两棵二叉树是否相同 +class Solution { +public: + bool isSameTree(TreeNode* p, TreeNode* q) { + // 判断一对节点是否相同 + if (p == nullptr && q == nullptr) { + return true; + } + if (p == nullptr || q == nullptr) { + return false; + } + if (p->val != q->val) { + return false; + } + // 判断其他节点是否相同 + return isSameTree(p->left, q->left) && isSameTree(p->right, q->right); + } +}; +``` + +```go +// by chatGPT (go) +// 定义:输入两个根节点,返回以它们为根的两棵二叉树是否相同 + func isSameTree(p *TreeNode, q *TreeNode) bool { + // 判断一对节点是否相同 + if p == nil && q == nil { + return true + } + if p == nil || q == nil { + return false + } + if p.Val != q.Val { + return false + } + // 判断其他节点是否相同 + return isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right) + } +``` + +```java +// by labuladong (java) +class Solution { + // 定义:输入两个根节点,返回以它们为根的两棵二叉树是否相同 + public boolean isSameTree(TreeNode p, TreeNode q) { + // 判断一对节点是否相同 + if (p == null && q == null) { + return true; + } + if (p == null || q == null) { + return false; + } + if (p.val != q.val) { + return false; + } + // 判断其他节点是否相同 + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 定义:输入两个根节点,返回以它们为根的两棵二叉树是否相同 + * @param {TreeNode} p + * @param {TreeNode} q + * @return {boolean} + */ +var isSameTree = function(p, q) { + // 判断一对节点是否相同 + if (p == null && q == null) { + return true; + } + if (p == null || q == null) { + return false; + } + if (p.val != q.val) { + return false; + } + // 判断其他节点是否相同 + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); +}; +``` + +```python +# by chatGPT (python) +# 定义:输入两个根节点,返回以它们为根的两棵二叉树是否相同 +class Solution: + def isSameTree(self, p: TreeNode, q: TreeNode) -> bool: + # 判断一对节点是否相同 + if p == None and q == None: + return True + if p == None or q == None: + return False + if p.val != q.val: + return False + # 判断其他节点是否相同 + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) +``` + +https://leetcode.cn/problems/same-tree 的多语言解法👆 + +https://leetcode.cn/problems/satisfiability-of-equality-equations 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool equationsPossible(vector& equations) { + // 26 个英文字母 + UF uf(26); + // 先让相等的字母形成连通分量 + for (string eq : equations) { + if (eq[1] == '=') { + char x = eq[0]; + char y = eq[3]; + uf.union_(x - 'a', y - 'a'); + } + } + // 检查不等关系是否打破相等关系的连通性 + for (string eq : equations) { + if (eq[1] == '!') { + char x = eq[0]; + char y = eq[3]; + // 如果相等关系成立,就是逻辑冲突 + if (uf.connected(x - 'a', y - 'a')) + return false; + } + } + return true; + } +}; + +class UF { +public: + // 记录连通分量个数 + int count; + // 存储若干棵树 + vector parent; + // 记录树的“重量” + vector size; + + UF(int n) { + this->count = n; + parent.resize(n); + size.resize(n); + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + /* 将 p 和 q 连通 */ + void union_(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + count--; + } + + /* 判断 p 和 q 是否互相连通 */ + bool connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + int getCount() { + return count; + } +}; +``` + +```go +// by chatGPT (go) +type UF struct { + // 记录连通分量个数 + Count int + // 存储若干棵树 + Parent []int + // 记录树的“重量” + Size []int +} + +func NewUF(n int) *UF { + u := UF{} + u.Count = n + u.Parent = make([]int, n) + u.Size = make([]int, n) + for i := 0; i < n; i++ { + u.Parent[i] = i + u.Size[i] = 1 + } + return &u +} + +/* 将 p 和 q 连通 */ +func (u *UF) Union(p, q int) { + rootP := u.find(p) + rootQ := u.find(q) + if rootP == rootQ { + return + } + + // 小树接到大树下面,较平衡 + if u.Size[rootP] > u.Size[rootQ] { + u.Parent[rootQ] = rootP + u.Size[rootP] += u.Size[rootQ] + } else { + u.Parent[rootP] = rootQ + u.Size[rootQ] += u.Size[rootP] + } + u.Count-- +} + +/* 判断 p 和 q 是否互相连通 */ +func (u *UF) Connected(p, q int) bool { + rootP := u.find(p) + rootQ := u.find(q) + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ +} + +/* 返回节点 x 的根节点 */ +func (u *UF) find(x int) int { + for u.Parent[x] != x { + // 进行路径压缩 + u.Parent[x] = u.Parent[u.Parent[x]] + x = u.Parent[x] + } + return x +} + +func equationsPossible(equations []string) bool { + // 26 个英文字母 + uf := NewUF(26) + // 先让相等的字母形成连通分量 + for _, eq := range equations { + if eq[1] == '=' { + x := eq[0] + y := eq[3] + uf.Union(int(x-'a'), int(y-'a')) + } + } + // 检查不等关系是否打破相等关系的连通性 + for _, eq := range equations { + if eq[1] == '!' { + x := eq[0] + y := eq[3] + // 如果相等关系成立,就是逻辑冲突 + if uf.Connected(int(x-'a'), int(y-'a')) { + return false + } + } + } + return true +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean equationsPossible(String[] equations) { + // 26 个英文字母 + UF uf = new UF(26); + // 先让相等的字母形成连通分量 + for (String eq : equations) { + if (eq.charAt(1) == '=') { + char x = eq.charAt(0); + char y = eq.charAt(3); + uf.union(x - 'a', y - 'a'); + } + } + // 检查不等关系是否打破相等关系的连通性 + for (String eq : equations) { + if (eq.charAt(1) == '!') { + char x = eq.charAt(0); + char y = eq.charAt(3); + // 如果相等关系成立,就是逻辑冲突 + if (uf.connected(x - 'a', y - 'a')) + return false; + } + } + return true; + } +} + +class UF { + // 记录连通分量个数 + private int count; + // 存储若干棵树 + private int[] parent; + // 记录树的“重量” + private int[] size; + + public UF(int n) { + this.count = n; + parent = new int[n]; + size = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + /* 将 p 和 q 连通 */ + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + count--; + } + + /* 判断 p 和 q 是否互相连通 */ + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + private int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + public int count() { + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +var equationsPossible = function(equations) { + // 26 个英文字母 + let uf = new ufFunc(26); + // 先让相等的字母形成连通分量 + for (let i = 0 ; i this.size[rootQ]) { + this.parent[rootQ] = rootP; + this.size[rootP] += this.size[rootQ]; + } else { + this.parent[rootP] = rootQ; + this.size[rootQ] += this.size[rootP]; + } + this.count--; + } + + /* 判断 p 和 q 是否互相连通 */ + connected(p, q) { + let rootP = this.find(p); + let rootQ = this.find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + find(x) { + while (this.parent[x] != x) { + // 进行路径压缩 + this.parent[x] = this.parent[this.parent[x]]; + x = this.parent[x]; + } + return x; + } + + count() { + return this.count; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def equationsPossible(self, equations: List[str]) -> bool: + uf = UF(26) #26个字母 + # 先让相等的字母形成连通分量 + for eq in equations: + if eq[1] == "=": + x = ord(eq[0]) - ord('a') + y = ord(eq[3]) - ord('a') + uf.union(x, y) + + # 检查不等关系是否打破相等关系的连通性 + for eq in equations: + if eq[1] == "!": + x = ord(eq[0]) - ord('a') + y = ord(eq[3]) - ord('a') + # 如果相等关系成立,就是逻辑冲突 + if uf.connected(x, y): + return False + return True + +class UF: + # 记录连通分量个数 + def __init__(self, n): + self.count = n + # 存储若干棵树 + self.parent = [i for i in range(n)] + # 记录树的“重量” + self.size = [1] * n + + # 将 p 和 q 连通 + def union(self, p, q): + rootP = self.find(p) + rootQ = self.find(q) + if rootP == rootQ: + return + + # 小树接到大树下面,较平衡 + if self.size[rootP] > self.size[rootQ]: + self.parent[rootQ] = rootP + self.size[rootP] += self.size[rootQ] + else: + self.parent[rootP] = rootQ + self.size[rootQ] += self.size[rootP] + self.count -= 1 + + # 判断 p 和 q 是否互相连通 + def connected(self, p, q): + rootP = self.find(p) + rootQ = self.find(q) + # 处于同一棵树上的节点,相互连通 + return rootP == rootQ + + # 返回节点 x 的根节点 + def find(self, x): + while self.parent[x] != x: + # 进行路径压缩 + self.parent[x] = self.parent[self.parent[x]] + x = self.parent[x] + return x + + def count(self): + return self.count +``` + +https://leetcode.cn/problems/satisfiability-of-equality-equations 的多语言解法👆 + +https://leetcode.cn/problems/search-a-2d-matrix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool searchMatrix(vector>& matrix, int target) { + int m = matrix.size(), n = matrix[0].size(); + // 把二维数组映射到一维 + int left = 0, right = m * n - 1; + // 前文讲的标准的二分搜索框架 + while(left <= right) { + int mid = left + (right - left) / 2; + if(get(matrix, mid) == target) + return true; + else if (get(matrix, mid) < target) + left = mid + 1; + else if (get(matrix, mid) > target) + right = mid - 1; + } + return false; + } + + // 通过一维坐标访问二维数组中的元素 + int get(vector>& matrix, int index) { + int m = matrix.size(), n = matrix[0].size(); + // 计算二维中的横纵坐标 + int i = index / n, j = index % n; + return matrix[i][j]; + } +}; +``` + +```go +// by chatGPT (go) +func searchMatrix(matrix [][]int, target int) bool { + m, n := len(matrix), len(matrix[0]) + // 把二维数组映射到一维 + left, right := 0, m*n-1 + // 前文讲的标准的二分搜索框架 + for left <= right { + mid := left + (right-left)/2 + if get(matrix, mid) == target { + return true + } else if get(matrix, mid) < target { + left = mid + 1 + } else if get(matrix, mid) > target { + right = mid - 1 + } + } + return false +} + +// 通过一维坐标访问二维数组中的元素 +func get(matrix [][]int, index int) int { + n := len(matrix[0]) + // 计算二维中的横纵坐标 + i, j := index/n, index%n + return matrix[i][j] +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean searchMatrix(int[][] matrix, int target) { + int m = matrix.length, n = matrix[0].length; + // 把二维数组映射到一维 + int left = 0, right = m * n - 1; + // 前文讲的标准的二分搜索框架 + while(left <= right) { + int mid = left + (right - left) / 2; + if(get(matrix, mid) == target) + return true; + else if (get(matrix, mid) < target) + left = mid + 1; + else if (get(matrix, mid) > target) + right = mid - 1; + } + return false; + } + + // 通过一维坐标访问二维数组中的元素 + int get(int[][] matrix, int index) { + int m = matrix.length, n = matrix[0].length; + // 计算二维中的横纵坐标 + int i = index / n, j = index % n; + return matrix[i][j]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var searchMatrix = function(matrix, target) { + var m = matrix.length, n = matrix[0].length; + // 把二维数组映射到一维 + var left = 0, right = m * n - 1; + // 前文讲的标准的二分搜索框架 + while(left <= right) { + var mid = left + Math.floor((right - left) / 2); + if(get(matrix, mid) == target) + return true; + else if (get(matrix, mid) < target) + left = mid + 1; + else if (get(matrix, mid) > target) + right = mid - 1; + } + return false; +}; + +// 通过一维坐标访问二维数组中的元素 +var get = function(matrix, index) { + var m = matrix.length, n = matrix[0].length; + // 计算二维中的横纵坐标 + var i = Math.floor(index / n), j = index % n; + return matrix[i][j]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def searchMatrix(self, matrix: List[List[int]], target: int) -> bool: + m, n = len(matrix), len(matrix[0]) + # 把二维数组映射到一维 + left, right = 0, m * n - 1 + # 前文讲的标准的二分搜索框架 + while(left <= right): + mid = left + (right - left) // 2 + if(self.get(matrix, mid) == target): + return True + elif self.get(matrix, mid) < target: + left = mid + 1 + elif self.get(matrix, mid) > target: + right = mid - 1 + return False + + # 通过一维坐标访问二维数组中的元素 + def get(self, matrix: List[List[int]], index: int) -> int: + m, n = len(matrix), len(matrix[0]) + # 计算二维中的横纵坐标 + i, j = index // n, index % n + return matrix[i][j] +``` + +https://leetcode.cn/problems/search-a-2d-matrix 的多语言解法👆 + +https://leetcode.cn/problems/search-a-2d-matrix-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool searchMatrix(vector>& matrix, int target) { + int m = matrix.size(), n = matrix[0].size(); + // 初始化在右上角 + int i = 0, j = n - 1; + while (i < m && j >= 0) { + if (matrix[i][j] == target) { + return true; + } + if (matrix[i][j] < target) { + // 需要大一点,往下移动 + i++; + } else { + // 需要小一点,往左移动 + j--; + } + } + // while 循环中没有找到,则 target 不存在 + return false; + } +}; +``` + +```go +// by chatGPT (go) +func searchMatrix(matrix [][]int, target int) bool { + m, n := len(matrix), len(matrix[0]) + // 初始化在右上角 + i, j := 0, n-1 + for i < m && j >= 0 { + if matrix[i][j] == target { + return true + } + if matrix[i][j] < target { + // 需要大一点,往下移动 + i++ + } else { + // 需要小一点,往左移动 + j-- + } + } + // for 循环中没有找到,则 target 不存在 + return false +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean searchMatrix(int[][] matrix, int target) { + int m = matrix.length, n = matrix[0].length; + // 初始化在右上角 + int i = 0, j = n - 1; + while (i < m && j >= 0) { + if (matrix[i][j] == target) { + return true; + } + if (matrix[i][j] < target) { + // 需要大一点,往下移动 + i++; + } else { + // 需要小一点,往左移动 + j--; + } + } + // while 循环中没有找到,则 target 不存在 + return false; + } +} +``` + +```javascript +// by chatGPT (javascript) +var searchMatrix = function(matrix, target) { + var m = matrix.length, n = matrix[0].length; + // 初始化在右上角 + var i = 0, j = n - 1; + while (i < m && j >= 0) { + if (matrix[i][j] == target) { + return true; + } + if (matrix[i][j] < target) { + // 需要大一点,往下移动 + i++; + } else { + // 需要小一点,往左移动 + j--; + } + } + // while 循环中没有找到,则 target 不存在 + return false; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def searchMatrix(self, matrix: List[List[int]], target: int) -> bool: + m, n = len(matrix), len(matrix[0]) + # 初始化在右上角 + i, j = 0, n - 1 + while i < m and j >= 0: + if matrix[i][j] == target: + return True + if matrix[i][j] < target: + # 需要大一点,往下移动 + i += 1 + else: + # 需要小一点,往左移动 + j -= 1 + # while 循环中没有找到,则 target 不存在 + return False +``` + +https://leetcode.cn/problems/search-a-2d-matrix-ii 的多语言解法👆 + +https://leetcode.cn/problems/search-in-a-binary-search-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + TreeNode* searchBST(TreeNode* root, int target) { + if (root == NULL) { + return NULL; + } + // 去左子树搜索 + if (root->val > target) { + return searchBST(root->left, target); + } + // 去右子树搜索 + if (root->val < target) { + return searchBST(root->right, target); + } + return root; + } +}; +``` + +```go +// by chatGPT (go) +func searchBST(root *TreeNode, target int) *TreeNode { + if root == nil { + return nil + } + // 去左子树搜索 + if root.Val > target { + return searchBST(root.Left, target) + } + // 去右子树搜索 + if root.Val < target { + return searchBST(root.Right, target) + } + return root +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode searchBST(TreeNode root, int target) { + if (root == null) { + return null; + } + // 去左子树搜索 + if (root.val > target) { + return searchBST(root.left, target); + } + // 去右子树搜索 + if (root.val < target) { + return searchBST(root.right, target); + } + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @param {number} val + * @return {TreeNode} + */ +var searchBST = function(root, val) { + if (root === null) { + return null; + } + // 去左子树搜索 + if (root.val > val) { + return searchBST(root.left, val); + } + // 去右子树搜索 + if (root.val < val) { + return searchBST(root.right, val); + } + return root; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def searchBST(self, root: TreeNode, target: int) -> TreeNode: + if not root: + return None + # 去左子树搜索 + if root.val > target: + return self.searchBST(root.left, target) + # 去右子树搜索 + if root.val < target: + return self.searchBST(root.right, target) + return root +``` + +https://leetcode.cn/problems/search-in-a-binary-search-tree 的多语言解法👆 + +https://leetcode.cn/problems/search-insert-position 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int searchInsert(vector& nums, int target) { + return left_bound(nums, target); + } + + // 搜索左侧边界的二分算法 + int left_bound(vector& nums, int target) { + if (nums.size() == 0) return -1; + int left = 0; + int right = nums.size(); // 注意 + + while (left < right) { // 注意 + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; // 注意 + } + } + return left; + } +}; +``` + +```go +// by chatGPT (go) +import "fmt" + +func searchInsert(nums []int, target int) int { + return leftBound(nums, target) +} + +// leftBound is a binary search algorithm that searches for the left boundary +func leftBound(nums []int, target int) int { + if len(nums) == 0 { + return -1 + } + left, right := 0, len(nums) // 注意 + + for left < right { // 注意 + mid := left + (right-left)/2 + if nums[mid] == target { + right = mid + } else if nums[mid] < target { + left = mid + 1 + } else if nums[mid] > target { + right = mid // 注意 + } + } + return left +} +``` + +```java +// by labuladong (java) +class Solution { + public int searchInsert(int[] nums, int target) { + return left_bound(nums, target); + } + + // 搜索左侧边界的二分算法 + int left_bound(int[] nums, int target) { + if (nums.length == 0) return -1; + int left = 0; + int right = nums.length; // 注意 + + while (left < right) { // 注意 + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; // 注意 + } + } + return left; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @param {number} target + * @return {number} + */ +var searchInsert = function(nums, target) { + return left_bound(nums, target); +}; + +/** + * 搜索左侧边界的二分算法 + * @param {number[]} nums + * @param {number} target + * @return {number} + */ +var left_bound = function(nums, target) { + if (nums.length == 0) return -1; + let left = 0; + let right = nums.length; // 注意 + + while (left < right) { // 注意 + let mid = left + Math.floor((right - left) / 2); + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; // 注意 + } + } + return left; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + return self.left_bound(nums, target) + + # 搜索左侧边界的二分算法 + def left_bound(self, nums: List[int], target: int) -> int: + if not nums: + return -1 + left = 0 + right = len(nums) # 注意 + + while left < right: # 注意 + mid = left + (right - left) // 2 + if nums[mid] == target: + right = mid + elif nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid # 注意 + + return left +``` + +https://leetcode.cn/problems/search-insert-position 的多语言解法👆 + +https://leetcode.cn/problems/seat-reservation-manager 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class SeatManager { + // 利用优先级队列自动排序,队头的元素就是最小的 + priority_queue, greater> pq; + +public: + SeatManager(int n) { + // 初始化所有空闲座位 + for (int i = 1; i <= n; i++) { + pq.push(i); + } + } + + int reserve() { + // 拿出队头元素(最小) + int seat = pq.top(); + pq.pop(); + return seat; + } + + void unreserve(int i) { + pq.push(i); + } +}; +``` + +```go +// by chatGPT (go) +import "container/heap" + +// 定义一个数组类型,并重写 Len(), Less(), Swap() 方法,实现堆 heap 的接口 +type PriorityQueue []int + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i] < pq[j] +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + *pq = append(*pq, x.(int)) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + x := old[n-1] + *pq = old[0 : n-1] + return x +} + +type SeatManager struct { + pq PriorityQueue +} + +func Constructor(n int) SeatManager { + pq := make([]int, n) + for i := 0; i < n; i++ { + pq[i] = i + 1 + } + return SeatManager{pq: pq} +} + +func (this *SeatManager) Reserve() int { + return heap.Pop(&this.pq).(int) +} + +func (this *SeatManager) Unreserve(seatNumber int) { + heap.Push(&this.pq, seatNumber) +} +``` + +```java +// by labuladong (java) +class SeatManager { + // 利用优先级队列自动排序,队头的元素就是最小的 + PriorityQueue pq = new PriorityQueue<>(); + + public SeatManager(int n) { + // 初始化所有空闲座位 + for (int i = 1; i <= n; i++) { + pq.offer(i); + } + } + + public int reserve() { + // 拿出队头元素(最小) + return pq.poll(); + } + + public void unreserve(int i) { + pq.offer(i); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + */ +var SeatManager = function(n) { + /** + * 利用优先队列(最小堆)自动排序,队头的元素就是最小的 + * PriorityQueue 是 Java 里的一个内置类,JavaScript 中没有 + * 可以手写实现一个最小堆,也可以转换为数组,使用数组 API 来模拟优先队列 + */ + this.pq = []; + for (let i = 1; i <= n; i++) { + this.pq.push(i); + } + this.pq.sort((a, b) => a - b); +}; + +/** + * @return {number} + */ +SeatManager.prototype.reserve = function() { + /** + * 拿出队头元素(最小) + */ + return this.pq.shift(); +}; + +/** + * @param {number} i + * @return {void} + */ +SeatManager.prototype.unreserve = function(i) { + this.pq.push(i); + this.pq.sort((a, b) => a - b); +}; + +/** + * Your SeatManager object will be instantiated and called as such: + * var obj = new SeatManager(n) + * var param_1 = obj.reserve() + * obj.unreserve(i) + */ +``` + +```python +# by chatGPT (python) +import heapq + +class SeatManager: + # 利用优先级队列自动排序,队头的元素就是最小的 + def __init__(self, n: int): + self.pq = [i for i in range(1,n+1)] + heapq.heapify(self.pq) + + def reserve(self) -> int: + # 拿出队头元素(最小) + return heapq.heappop(self.pq) + + def unreserve(self, seatNumber: int) -> None: + heapq.heappush(self.pq, seatNumber) +``` + +https://leetcode.cn/problems/seat-reservation-manager 的多语言解法👆 + +https://leetcode.cn/problems/serialize-and-deserialize-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Codec { +public: + string SEP = ","; + string NULL_STR = "#"; + + /* 主函数,将二叉树序列化为字符串 */ + string serialize(TreeNode* root) { + if(!root) return string(NULL_STR) + SEP; + /* 前序遍历位置 */ + string res = to_string(root->val) + SEP; + /******************/ + res += serialize(root->left); + res += serialize(root->right); + return res; + } + + /* 主函数,将字符串反序列化为二叉树结构 */ + TreeNode* deserialize(string data) { + LinkedList nodes; + string cur = ""; + for(char &c: data) { + if(c == SEP) { + nodes.push_back(cur); + cur = ""; + } else cur += c; + } + return deserialize(nodes); + } + + /* 辅助函数,通过 nodes 列表构造二叉树 */ + TreeNode* deserialize(LinkedList &nodes) { + if(nodes.empty()) return NULL_STRptr; + + /* 前序遍历位置 */ + string first = nodes.front(); nodes.pop_front(); + if(first == NULL_STR) return NULL_STRptr; + TreeNode* root = new TreeNode(stoi(first)); + /******************/ + + root->left = deserialize(nodes); + root->right = deserialize(nodes); + + return root; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "strconv" + "strings" +) + + +type Codec struct { + SEP string + NULL string +} + +func Constructor() Codec { + return Codec{ + SEP: ",", + NULL: "#", + } +} + +/* 辅助函数,将二叉树存入 StringBuilder */ +func (this *Codec) serialize(root *TreeNode, sb *strings.Builder) { + if root == nil { + sb.WriteString(this.NULL) + sb.WriteString(this.SEP) + return + } + + /******前序遍历位置******/ + sb.WriteString(strconv.Itoa(root.Val)) + sb.WriteString(this.SEP) + /***********************/ + + this.serialize(root.Left, sb) + this.serialize(root.Right, sb) +} + +/* 主函数,将二叉树序列化为字符串 */ +func (this *Codec) serializeTree(root *TreeNode) string { + sb := &strings.Builder{} + this.serialize(root, sb) + return sb.String() +} + +/* 辅助函数,通过 nodes 列表构造二叉树 */ +func (this *Codec) deserialize(nodes *[]string) *TreeNode { + if len(*nodes) == 0 { + return nil + } + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + first := (*nodes)[0] + *nodes = (*nodes)[1:] + if first == this.NULL { + return nil + } + root := &TreeNode{ + Val: atoi(first), + } + /***********************/ + + root.Left = this.deserialize(nodes) + root.Right = this.deserialize(nodes) + + return root +} + +/* 主函数,将字符串反序列化为二叉树结构 */ +func (this *Codec) deserializeTree(data string) *TreeNode { + // 将字符串转化成列表 + nodes := strings.Split(data, this.SEP) + return this.deserialize(&nodes) +} + +func atoi(s string) int { + n, _ := strconv.Atoi(s) + return n +} +``` + +```java +// by labuladong (java) +public class Codec { + String SEP = ","; + String NULL = "#"; + + /* 主函数,将二叉树序列化为字符串 */ + public String serialize(TreeNode root) { + StringBuilder sb = new StringBuilder(); + serialize(root, sb); + return sb.toString(); + } + + /* 辅助函数,将二叉树存入 StringBuilder */ + void serialize(TreeNode root, StringBuilder sb) { + if (root == null) { + sb.append(NULL).append(SEP); + return; + } + + /******前序遍历位置******/ + sb.append(root.val).append(SEP); + /***********************/ + + serialize(root.left, sb); + serialize(root.right, sb); + } + + /* 主函数,将字符串反序列化为二叉树结构 */ + public TreeNode deserialize(String data) { + // 将字符串转化成列表 + LinkedList nodes = new LinkedList<>(); + for (String s : data.split(SEP)) { + nodes.addLast(s); + } + return deserialize(nodes); + } + + /* 辅助函数,通过 nodes 列表构造二叉树 */ + TreeNode deserialize(LinkedList nodes) { + if (nodes.isEmpty()) return null; + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + String first = nodes.removeFirst(); + if (first.equals(NULL)) return null; + TreeNode root = new TreeNode(Integer.parseInt(first)); + /***********************/ + + root.left = deserialize(nodes); + root.right = deserialize(nodes); + + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @constructor + */ +var Codec = function() { + // 用于连接节点字符串的分隔符 + this.SEP = ","; + // 空节点的表示 + this.NULL = "#"; +}; + +/** + * Encodes a tree to a single string. + * + * @param {TreeNode} root + * @return {string} + */ +Codec.prototype.serialize = function(root) { + var sb = []; + this.serializeHelper(root, sb); + return sb.join(""); +}; + +/** + * 将字符串反序列化为二叉树结构 + * Decodes your encoded data to tree. + * + * @param {string} data + * @return {TreeNode} + */ +Codec.prototype.deserialize = function(data) { + // 将字符串转化成列表 + var nodes = data.split(this.SEP); + return this.deserializeHelper(nodes); +}; + +Codec.prototype.serializeHelper = function(root, sb) { + if (root === null) { + sb.push(this.NULL, this.SEP); + return; + } + // 前序遍历位置 + sb.push(root.val, this.SEP); + this.serializeHelper(root.left, sb); + this.serializeHelper(root.right, sb); +}; + +Codec.prototype.deserializeHelper = function(nodes) { + if (nodes.length === 0) return null; + // 前序遍历位置:列表最左侧就是根节点 + var val = nodes.shift(); + if (val === this.NULL) return null; + var root = new TreeNode(parseInt(val)); + root.left = this.deserializeHelper(nodes); + root.right = this.deserializeHelper(nodes); + return root; +}; +``` + +```python +# by chatGPT (python) +class Codec: + def __init__(self): + self.SEP = ',' + self.NULL = '#' + + # 将二叉树序列化为字符串 + def serialize(self, root: TreeNode) -> str: + sb = [] + self._serialize(root, sb) + return ''.join(sb) + + # 将二叉树存入列表 + def _serialize(self, root, sb): + if not root: + sb.append(self.NULL).append(self.SEP) + return + + # 前序遍历位置 + sb.append(str(root.val)).append(self.SEP) + + self._serialize(root.left, sb) + self._serialize(root.right, sb) + + # 将字符串反序列化为二叉树结构 + def deserialize(self, data: str) -> TreeNode: + # 将字符串转化成列表 + nodes = data.split(self.SEP) + return self._deserialize(nodes) + + # 通过 nodes 列表构造二叉树 + def _deserialize(self, nodes): + if not nodes: + return None + + # 前序遍历位置 + val = nodes.pop(0) + if val == self.NULL: + return None + root = TreeNode(int(val)) + + root.left = self._deserialize(nodes) + root.right = self._deserialize(nodes) + + return root +``` + +https://leetcode.cn/problems/serialize-and-deserialize-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/set-mismatch 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector findErrorNums(vector& nums) { + int n=nums.size(); + int dup=-1; + for(int i=0;i0) + // 将索引转换成元素 + missing=i+1; + return {dup,missing}; + } +}; +``` + +```go +// by chatGPT (go) +func findErrorNums(nums []int) []int { + n := len(nums) + dup := -1 + for i := 0; i < n; i++ { + // 现在的元素是从 1 开始的 + index := abs(nums[i]) - 1 + if nums[index] < 0 { + dup = abs(nums[i]) + } else { + nums[index] *= -1 + } + } + + missing := -1 + for i := 0; i < n; i++ { + if nums[i] > 0 { + // 将索引转换成元素 + missing = i + 1 + } + } + + return []int{dup, missing} +} + +func abs(a int) int { + if a < 0 { + return -a + } + return a +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] findErrorNums(int[] nums) { + int n = nums.length; + int dup = -1; + for (int i = 0; i < n; i++) { + // 现在的元素是从 1 开始的 + int index = Math.abs(nums[i]) - 1; + if (nums[index] < 0) + dup = Math.abs(nums[i]); + else + nums[index] *= -1; + } + + int missing = -1; + for (int i = 0; i < n; i++) + if (nums[i] > 0) + // 将索引转换成元素 + missing = i + 1; + + return new int[]{dup, missing}; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number[]} + */ +var findErrorNums = function(nums) { + var n = nums.length; + var dup = -1; + for (var i = 0; i < n; i++) { + // 现在的元素是从 1 开始的 + var index = Math.abs(nums[i]) - 1; + if (nums[index] < 0) + dup = Math.abs(nums[i]); + else + nums[index] *= -1; + } + + var missing = -1; + for (var i = 0; i < n; i++) + if (nums[i] > 0) + // 将索引转换成元素 + missing = i + 1; + + return [dup, missing]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findErrorNums(self, nums: List[int]) -> List[int]: + n = len(nums) + dup = -1 + for i in range(n): + # 现在的元素是从 1 开始的 + index = abs(nums[i]) - 1 + if nums[index] < 0: + dup = abs(nums[i]) + else: + nums[index] *= -1 + + missing = -1 + for i in range(n): + if nums[i] > 0: + # 将索引转换成元素 + missing = i + 1 + + return [dup, missing] +``` + +https://leetcode.cn/problems/set-mismatch 的多语言解法👆 + +https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + ListNode* deleteNode(ListNode* head, int val) { + // 存放删除 val 的链表 + ListNode* dummy = new ListNode(-1); + // q 指针负责生成结果链表 + ListNode* q = dummy; + // p 负责遍历原链表 + ListNode* p = head; + while (p != nullptr) { + if (p->val != val) { + // 把值不为 val 的节点接到结果链表上 + q->next = p; + q = q->next; + } + // 断开原链表中的每个节点的 next 指针 + ListNode* temp = p->next; + p->next = nullptr; + p = temp; + } + + return dummy->next; + } +}; +``` + +```go +// by chatGPT (go) +func deleteNode(head *ListNode, val int) *ListNode { + // 存放删除 val 的链表 + dummy := &ListNode{ + Val: -1, + Next: nil, + } + // q 指针负责生成结果链表 + q := dummy + // p 负责遍历原链表 + p := head + for p != nil { + if p.Val != val { + // 把值不为 val 的节点接到结果链表上 + q.Next = p + q = q.Next + } + // 断开原链表中的每个节点的 next 指针 + temp := p.Next + p.Next = nil + p = temp + } + + return dummy.Next +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode deleteNode(ListNode head, int val) { + // 存放删除 val 的链表 + ListNode dummy = new ListNode(-1); + // q 指针负责生成结果链表 + ListNode q = dummy; + // p 负责遍历原链表 + ListNode p = head; + while (p != null) { + if (p.val != val) { + // 把值不为 val 的节点接到结果链表上 + q.next = p; + q = q.next; + } + // 断开原链表中的每个节点的 next 指针 + ListNode temp = p.next; + p.next = null; + p = temp; + } + + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {ListNode} head + * @param {number} val + * @return {ListNode} + */ +var deleteNode = function(head, val) { + // 存放删除 val 的链表 + let dummy = new ListNode(-1); + // q 指针负责生成结果链表 + let q = dummy; + // p 负责遍历原链表 + let p = head; + while (p != null) { + if (p.val != val) { + // 把值不为 val 的节点接到结果链表上 + q.next = p; + q = q.next; + } + // 断开原链表中的每个节点的 next 指针 + let temp = p.next; + p.next = null; + p = temp; + } + + return dummy.next; +}; +``` + +```python +# by chatGPT (python) +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next + +class Solution: + def deleteNode(self, head: ListNode, val: int) -> ListNode: + # 存放删除 val 的链表 + dummy = ListNode(-1) + # q 指针负责生成结果链表 + q = dummy + # p 负责遍历原链表 + p = head + while p: + if p.val != val: + # 把值不为 val 的节点接到结果链表上 + q.next = p + q = q.next + # 断开原链表中的每个节点的 next 指针 + temp = p.next + p.next = None + p = temp + + return dummy.next +``` + +https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof 的多语言解法👆 + +https://leetcode.cn/problems/shortest-path-in-binary-matrix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int shortestPathBinaryMatrix(vector>& grid) { + int m = grid.size(), n = grid[0].size(); + if (grid[0][0] == 1 || grid[m - 1][n - 1] == 1) { + return -1; + } + + queue> q; + // 需要记录走过的路径,避免死循环 + vector> visited(m, vector(n, false)); + + // 初始化队列,从 (0, 0) 出发 + q.push({0, 0}); + visited[0][0] = true; + int pathLen = 1; + + // 执行 BFS 算法框架,从值为 0 的坐标开始向八个方向扩散 + vector> dirs = { + {0, 1}, {0, -1}, {1, 0}, {-1, 0}, + {1, 1}, {1, -1}, {-1, 1}, {-1, -1} + }; + while (!q.empty()) { + int sz = q.size(); + for (int __ = 0; __ < sz; __++) { + auto cur = q.front(); + q.pop(); + int x = cur.first, y = cur.second; + if (x == m - 1 && y == n - 1) { + return pathLen; + } + // 向八个方向扩散 + for (int i = 0; i < 8; i++) { + int nextX = x + dirs[i][0]; + int nextY = y + dirs[i][1]; + // 确保相邻的这个坐标没有越界且值为 0 且之前没有走过 + if (nextX >= 0 && nextX < m && nextY >= 0 && nextY < n + && grid[nextX][nextY] == 0 && !visited[nextX][nextY]) { + q.push({nextX, nextY}); + visited[nextX][nextY] = true; + } + } + } + pathLen++; + } + return -1; + } +}; +``` + +```go +// by chatGPT (go) +func shortestPathBinaryMatrix(grid [][]int) int { + m, n := len(grid), len(grid[0]) + if grid[0][0] == 1 || grid[m-1][n-1] == 1 { + return -1 + } + + q := make([][]int, 0) + // 需要记录走过的路径,避免死循环 + visited := make([][]bool, m) + for i := 0; i < m; i++ { + visited[i] = make([]bool, n) + } + + // 初始化队列,从 (0, 0) 出发 + q = append(q, []int{0, 0}) + visited[0][0] = true + pathLen := 1 + + // 执行 BFS 算法框架,从值为 0 的坐标开始向八个方向扩散 + dirs := [][]int{ + {0, 1}, {0, -1}, {1, 0}, {-1, 0}, + {1, 1}, {1, -1}, {-1, 1}, {-1, -1}, + } + for len(q) > 0 { + sz := len(q) + for i := 0; i < sz; i++ { + cur := q[i] + x, y := cur[0], cur[1] + if x == m-1 && y == n-1 { + return pathLen + } + // 向八个方向扩散 + for j := 0; j < 8; j++ { + nextX := x + dirs[j][0] + nextY := y + dirs[j][1] + // 确保相邻的这个坐标没有越界且值为 0 且之前没有走过 + if nextX >= 0 && nextX < m && nextY >= 0 && nextY < n && + grid[nextX][nextY] == 0 && !visited[nextX][nextY] { + q = append(q, []int{nextX, nextY}) + visited[nextX][nextY] = true + } + } + } + pathLen++ + q = q[sz:] + } + return -1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int shortestPathBinaryMatrix(int[][] grid) { + int m = grid.length, n = grid[0].length; + if (grid[0][0] == 1 || grid[m - 1][n - 1] == 1) { + return -1; + } + + Queue q = new LinkedList<>(); + // 需要记录走过的路径,避免死循环 + boolean[][] visited = new boolean[m][n]; + + // 初始化队列,从 (0, 0) 出发 + q.offer(new int[]{0, 0}); + visited[0][0] = true; + int pathLen = 1; + + // 执行 BFS 算法框架,从值为 0 的坐标开始向八个方向扩散 + int[][] dirs = new int[][]{ + {0, 1}, {0, -1}, {1, 0}, {-1, 0}, + {1, 1}, {1, -1}, {-1, 1}, {-1, -1} + }; + while (!q.isEmpty()) { + int sz = q.size(); + for (int __ = 0; __ < sz; __++) { + int[] cur = q.poll(); + int x = cur[0], y = cur[1]; + if (x == m - 1 && y == n - 1) { + return pathLen; + } + // 向八个方向扩散 + for (int i = 0; i < 8; i++) { + int nextX = x + dirs[i][0]; + int nextY = y + dirs[i][1]; + // 确保相邻的这个坐标没有越界且值为 0 且之前没有走过 + if (nextX >= 0 && nextX < m && nextY >= 0 && nextY < n + && grid[nextX][nextY] == 0 && !visited[nextX][nextY]) { + q.offer(new int[]{nextX, nextY}); + visited[nextX][nextY] = true; + } + } + } + pathLen++; + } + return -1; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} grid + * @return {number} + */ +var shortestPathBinaryMatrix = function(grid) { + const m = grid.length, n = grid[0].length; + if (grid[0][0] === 1 || grid[m - 1][n - 1] === 1) { + return -1; + } + + const q = []; + // 需要记录走过的路径,避免死循环 + const visited = new Array(m).fill(0).map(() => new Array(n).fill(false)); + + // 初始化队列,从 (0, 0) 出发 + q.push([0, 0]); + visited[0][0] = true; + let pathLen = 1; + + // 执行 BFS 算法框架,从值为 0 的坐标开始向八个方向扩散 + const dirs = [ + [0, 1], [0, -1], [1, 0], [-1, 0], + [1, 1], [1, -1], [-1, 1], [-1, -1] + ]; + while (q.length) { + const sz = q.length; + for (let __ = 0; __ < sz; __++) { + const cur = q.shift(); + const x = cur[0], y = cur[1]; + if (x === m - 1 && y === n - 1) { + return pathLen; + } + // 向八个方向扩散 + for (let i = 0; i < 8; i++) { + const nextX = x + dirs[i][0]; + const nextY = y + dirs[i][1]; + // 确保相邻的这个坐标没有越界且值为 0 且之前没有走过 + if (nextX >= 0 && nextX < m && nextY >= 0 && nextY < n + && grid[nextX][nextY] === 0 && !visited[nextX][nextY]) { + q.push([nextX, nextY]); + visited[nextX][nextY] = true; + } + } + } + pathLen++; + } + return -1; +}; +``` + +```python +# by chatGPT (python) +from collections import deque + +class Solution: + def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int: + # 获取矩阵大小 + m, n = len(grid), len(grid[0]) + # 如果起点或终点为障碍物,则无法到达终点 + if grid[0][0] == 1 or grid[m - 1][n - 1] == 1: + return -1 + + # 初始化队列、visited数组 + q = deque() + visited = [[False for _ in range(n)] for _ in range(m)] + + # 将起点加入队列,并标记为已访问 + q.append([0, 0]) + visited[0][0] = True + # 记录路径长度 + pathLen = 1 + + # 定义8个方向数组 + dirs = [[0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [1, -1], [-1, 1], [-1, -1]] + while q: + sz = len(q) + # 遍历当前层的所有结点 + for _ in range(sz): + cur = q.popleft() + x, y = cur[0], cur[1] + # 如果当前结点是目标点,则返回路径长度 + if x == m - 1 and y == n - 1: + return pathLen + # 向八个方向扩散 + for i in range(8): + nextX = x + dirs[i][0] + nextY = y + dirs[i][1] + # 确保相邻的这个坐标没有越界且值为 0 且之前没有走过 + if 0 <= nextX < m and 0 <= nextY < n and not visited[nextX][nextY] and grid[nextX][nextY] == 0: + q.append([nextX, nextY]) + visited[nextX][nextY] = True + # 层数增加 + pathLen += 1 + # 如果无法到达终点,则返回 -1 + return -1 +``` + +https://leetcode.cn/problems/shortest-path-in-binary-matrix 的多语言解法👆 + +https://leetcode.cn/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class MedianFinder { +private: + priority_queue, greater> large; // 小顶堆 + priority_queue, less> small; // 大顶堆 +public: + MedianFinder() {} + + /** + ![](../pictures/中位数/2.jpeg) + */ + double findMedian() { + if(large.size() < small.size()) { + return small.top(); + } else if(large.size() > small.size()) { + return large.top(); + } + return (large.top() + small.top()) / 2.0; //如果元素一样多,中位数就是两个堆顶元素平均数 + /** + ![](../pictures/中位数/1.jpeg) + */ + } + + void addNum(int num) { + if(small.size() >= large.size()) { + small.push(num); + large.push(small.top()); + small.pop(); + } else { + large.push(num); + small.push(large.top()); + large.pop(); + } + } +}; +``` + +```go +// by chatGPT (go) +func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode { + var carry int + var result *ListNode + var head *ListNode + for l1 != nil || l2 != nil || carry != 0 { + sum := 0 + if l1 != nil { + sum += l1.Val + l1 = l1.Next + } + if l2 != nil { + sum += l2.Val + l2 = l2.Next + } + sum += carry + carry = sum / 10 + node := &ListNode{Val: sum % 10} // 求和的处理 + if result == nil { + result = node + head = node + } else { + result.Next = node + result = result.Next + } + } + return head +} +``` + +```java +// by labuladong (java) +class MedianFinder { + private PriorityQueue large; + private PriorityQueue small; + + public MedianFinder() { + // 小顶堆 + large = new PriorityQueue<>(); + // 大顶堆 + small = new PriorityQueue<>((a, b) -> { + return b - a; + }); + } + + public double findMedian() { + /** + ![](../pictures/中位数/2.jpeg) + */ + // 如果元素不一样多,多的那个堆的堆顶元素就是中位数 + if (large.size() < small.size()) { + return small.peek(); + } else if (large.size() > small.size()) { + return large.peek(); + } + // 如果元素一样多,两个堆堆顶元素的平均数是中位数 + return (large.peek() + small.peek()) / 2.0; + /** + ![](../pictures/中位数/1.jpeg) + */ + } + + public void addNum(int num) { + if (small.size() >= large.size()) { + small.offer(num); + large.offer(small.poll()); + } else { + large.offer(num); + small.offer(large.poll()); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 现有一个按非递减顺序排序的整数数组 nums。 + * 请你删除重复出现的元素,使每个元素只出现一次。 + * 返回删除后数组的新长度。 + * 不要使用额外的数组空间,必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。 + * + * 给定 nums = [0,0,1,1,1,2,2,3,3,4], + * + * 返回 新长度5, 并且原数组 nums 的前五个元素为 0、1、2、3 和 4。 + * + * 你不需要考虑数组中超出新长度后面的元素。 + */ + +/** + * 思路很简单,因为数组已经是非递减排列的了,意思就是数组中之后会出现比当前下标的值还小的值都会在之前出现,所以我们可以考虑双指针法。 + * 既然想到了双指针,那么要开一个变量储存数组中不重复的个数,这个变量命名为NonDuplicate即可。开始的时候它的值是1,因为第一个数一定是不重复的。 + * 随后,我们遍历数组,比较i和i+1位置的值: + * if(nums[i]!==nums[i+1]),则说明num[i+1]是不重复的,此时就将nums[j]赋值为nums[i+1], 非重复数的个数NonDuplicate++ + * 最后再把nums数组中前NonDuplicate个数赋值为nums中各个元素,最后NonDuplicate就是数组中不重复的个数。 + * 时间复杂度:O(n),因为数组只遍历了一次。 + * 空间复杂度:O(1),没有利用额外的空间 + */ + +var removeDuplicates = function(nums) { + let NonDuplicate =1; + let n = nums.length; + let j=0; + for(let i=0; i float: + """ + + ![](../pictures/中位数/2.jpeg) + """ + # 如果元素不一样多,多的那个堆的堆顶元素就是中位数 + if self.large.qsize() < self.small.qsize(): + return self.small.queue[0] + elif self.large.qsize() > self.small.qsize(): + return self.large.queue[0] + # 如果元素一样多,两个堆堆顶元素的平均数是中位数 + return (self.large.queue[0] + self.small.queue[0]) / 2.0 + """ + + ![](../pictures/中位数/1.jpeg) + """ + + def addNum(self, num: int) -> None: + if self.small.qsize() >= self.large.qsize(): + self.small.put(num) + self.large.put(self.small.get()) + else: + self.large.put(num) + self.small.put(self.large.get()) +``` + +https://leetcode.cn/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/shu-zhi-de-zheng-shu-ci-fang-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + double myPow(double a, int k) { + if (k == 0) return 1; + + if (k == INT_MIN) { + // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a; + } + + if (k < 0) { + return myPow(1 / a, -k); + } + + if (k % 2 == 1) { + // k 是奇数 + return (a * myPow(a, k - 1)); + } else { + // k 是偶数 + double sub = myPow(a, k / 2); + return (sub * sub); + } + } +}; +``` + +```go +// by chatGPT (go) +func myPow(a float64, k int) float64 { + if k == 0 { // 如果 k 等于 0,直接返回 1 + return 1 + } + + if k == -2147483648 { // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a + } + + if k < 0 { // 如果 k 小于 0,递归转换成 1 / a 的相反数 + return myPow(1 / a, -k) + } + + if k % 2 == 1 { // 如果 k 是奇数,递归运算 k - 1 + return a * myPow(a, k - 1) + } else { // 如果 k 是偶数,递归运算 k / 2 + sub := myPow(a, k / 2) + return sub * sub + } +} +``` + +```java +// by labuladong (java) +class Solution { + public double myPow(double a, int k) { + if (k == 0) return 1; + + if (k == Integer.MIN_VALUE) { + // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a; + } + + if (k < 0) { + return myPow(1 / a, -k); + } + + if (k % 2 == 1) { + // k 是奇数 + return (a * myPow(a, k - 1)); + } else { + // k 是偶数 + double sub = myPow(a, k / 2); + return (sub * sub); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} a + * @param {number} k + * @return {number} + */ +var myPow = function(a, k) { + if (k === 0) return 1; + + if (k === -2147483648) { + // 把 k 是 INT_MIN 的情况单独拿出来处理 + // 避免 -k 整型溢出 + return myPow(1 / a, -(k + 1)) / a; + } + + if (k < 0) { + return myPow(1 / a, -k); + } + + if (k % 2 === 1) { + // k 是奇数 + return (a * myPow(a, k - 1)); + } else { + // k 是偶数 + var sub = myPow(a, k / 2); + return (sub * sub); + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def myPow(self, a: float, k: int) -> float: + if k == 0: + return 1 + + if k == -2147483648: # k 是 INT_MIN 的情况单独拿出来处理 + # 避免 -k 整型溢出 + return self.myPow(1 / a, -(k + 1)) / a + + if k < 0: + return self.myPow(1 / a, -k) + + if k % 2 == 1: # k 是奇数 + return a * self.myPow(a, k - 1) + else: # k 是偶数 + sub = self.myPow(a, k // 2) + return sub * sub +``` + +https://leetcode.cn/problems/shu-zhi-de-zheng-shu-ci-fang-lcof 的多语言解法👆 + +https://leetcode.cn/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findNthDigit(int n) { + if (n == 0) { + return 0; + } + return findNthDigit_100(n); + } + + // 第 400 题的解法代码 + int findNthDigit_400(int n) { + // 位数(一位数,两位数...) + int digit = 1; + // 1,10,100, 1000 这样的后缀 + long base = 1; + + while (n > 9 * base * digit) { + n -= 9 * base * digit; + base *= 10; + digit++; + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + long val = base + (n - 1) / digit; + // 是这个三位数的第几位呢?这样算: + int index = (n - 1) % digit; + + // 怎么把 val 的第 index 这一位数字抠出来呢?这样算: + return (to_string(val))[index] - '0'; + } + + int findNthDigit_100(int n) { + // 按位计数是从 0 开始的,故需先 -1 + n--; + + for (int bit = 1; bit <= 11; bit++) { + int firstNum = pow(10, bit - 1); + int lastNum = pow(10, bit) - 1; + int bitNums = lastNum - firstNum + 1; + int totalNums = bitNums * bit; + if (n < totalNums) { + int numTimes = n / bit; + int numIndex = n % bit; + int targetNum = firstNum + numTimes; + string targetStr = to_string(targetNum); + return targetStr[numIndex] - '0'; + } + n -= totalNums; + } + return -1; + } +}; +``` + +```go +// by chatGPT (go) +import "strconv" + +func findNthDigit(n int) int { + if n == 0 { + return 0 + } + return findNthDigit400(n) +} + +// 第 400 题的解法代码 +func findNthDigit400(n int) int { + // 位数(一位数,两位数...) + digit := 1 + // 1,10,100, 1000 这样的后缀 + base := 1 + for n > 9*base*digit { + n -= 9 * base * digit + base *= 10 + digit++ + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + val := base + (n-1)/digit + // 是这个三位数的第几位呢?这样算: + index := (n - 1) % digit + // 怎么把 val 的第 index 这一位数字抠出来呢?这样算: + return int(strconv.Itoa(val)[index] - '0') +} +``` + +```java +// by labuladong (java) +class Solution { + public int findNthDigit(int n) { + if (n == 0) { + return 0; + } + return findNthDigit_100(n); + } + + // 第 400 题的解法代码 + int findNthDigit_400(int n) { + // 位数(一位数,两位数...) + int digit = 1; + // 1,10,100, 1000 这样的后缀 + long base = 1; + + while (n > 9 * base * digit) { + n -= 9 * base * digit; + base *= 10; + digit++; + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + long val = base + (n - 1) / digit; + // 是这个三位数的第几位呢?这样算: + int index = (n - 1) % digit; + + // 怎么把 val 的第 index 这一位数字抠出来呢?这样算: + return ("" + val).charAt(index) - '0'; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @return {number} + */ +var findNthDigit = function(n) { + if (n == 0) { + return 0; + } + return findNthDigit_100(n); +}; + +// 第 400 题的解法代码 +var findNthDigit_400 = function(n) { + // 位数(一位数,两位数...) + var digit = 1; + // 1,10,100, 1000 这样的后缀 + var base = 1; + + while (n > 9 * base * digit) { + n -= 9 * base * digit; + base *= 10; + digit++; + } + + // 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + // 哪个三位数呢?这样算: + var val = base + Math.floor((n - 1) / digit); + // 是这个三位数的第几位呢?这样算: + var index = (n - 1) % digit; + + // 怎么把 val 的第 index 这一位数字抠出来呢?这样算: + return parseInt(("" + val).charAt(index)); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findNthDigit(self, n: int) -> int: + if n == 0: + return 0 + return self.findNthDigit_400(n) + + # 第 400 题的解法代码 + def findNthDigit_400(self, n: int) -> int: + # 位数(一位数,两位数...) + digit = 1 + # 1,10,100, 1000 这样的后缀 + base = 1 + + while n > 9 * base * digit: + n -= 9 * base * digit + base *= 10 + digit += 1 + + # 此时假设 base = 1000,那么说明 n 是 100~999 中的某个三位数的某一位 + # 哪个三位数呢?这样算: + val = base + (n - 1) // digit + # 是这个三位数的第几位呢?这样算: + index = (n - 1) % digit + + # 怎么把 val 的第 index 这一位数字抠出来呢?这样算: + return int(str(val)[index]) +``` + +https://leetcode.cn/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int majorityElement(vector& nums) { + // 我们想寻找的那个众数 + int target = 0; + // 计数器(类比带电粒子例子中的带电性) + int count = 0; + for (int i = 0; i < nums.size(); i++) { + if (count == 0) { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i]; + // 众数出现了一次 + count = 1; + } else if (nums[i] == target) { + // 如果遇到的是目标众数,计数器累加 + count++; + } else { + // 如果遇到的不是目标众数,计数器递减 + count--; + } + } + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target; + } +}; +``` + +```go +// by chatGPT (go) +func majorityElement(nums []int) int { + // 我们想寻找的那个众数 + target := 0 + // 计数器(类比带电粒子例子中的带电性) + count := 0 + + for i := 0; i < len(nums); i++ { + if count == 0 { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i] + // 众数出现了一次 + count = 1 + } else if nums[i] == target { + // 如果遇到的是目标众数,计数器累加 + count++ + } else { + // 如果遇到的不是目标众数,计数器递减 + count-- + } + } + + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target +} +``` + +```java +// by labuladong (java) +class Solution { + public int majorityElement(int[] nums) { + // 我们想寻找的那个众数 + int target = 0; + // 计数器(类比带电粒子例子中的带电性) + int count = 0; + for (int i = 0; i < nums.length; i++) { + if (count == 0) { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i]; + // 众数出现了一次 + count = 1; + } else if (nums[i] == target) { + // 如果遇到的是目标众数,计数器累加 + count++; + } else { + // 如果遇到的不是目标众数,计数器递减 + count--; + } + } + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target; + } +} +``` + +```javascript +// by chatGPT (javascript) +var majorityElement = function(nums) { + // 我们想寻找的那个众数 + let target = 0; + // 计数器(类比带电粒子例子中的带电性) + let count = 0; + for (let i = 0; i < nums.length; i++) { + if (count == 0) { + // 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i]; + // 众数出现了一次 + count = 1; + } else if (nums[i] == target) { + // 如果遇到的是目标众数,计数器累加 + count++; + } else { + // 如果遇到的不是目标众数,计数器递减 + count--; + } + } + // 回想带电粒子的例子 + // 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def majorityElement(self, nums: List[int]) -> int: + # 我们想寻找的那个众数 + target = 0 + # 计数器(类比带电粒子例子中的带电性) + count = 0 + for i in range(len(nums)): + if count == 0: + # 当计数器为 0 时,假设 nums[i] 就是众数 + target = nums[i] + # 众数出现了一次 + count = 1 + elif nums[i] == target: + # 如果遇到的是目标众数,计数器累加 + count += 1 + else: + # 如果遇到的不是目标众数,计数器递减 + count -= 1 + # 回想带电粒子的例子 + # 此时的 count 必然大于 0,此时的 target 必然就是目标众数 + return target +``` + +https://leetcode.cn/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int reversePairs(vector& nums) { + if (nums.size() == 0) { + return 0; + } + // 执行归并排序 + sort(nums); + return count; + } + + void sort(vector& nums) { + temp.resize(nums.size()); + sort(nums, 0, nums.size() - 1); + } + + // 归并排序 + void sort(vector& nums, int lo, int hi) { + if (lo == hi) { + return; + } + int mid = lo + (hi - lo) / 2; + sort(nums, lo, mid); + sort(nums, mid + 1, hi); + merge(nums, lo, mid, hi); + } + + // 记录「翻转对」的个数 + int count = 0; + + void merge(vector& nums, int lo, int mid, int hi) { + for (int i = lo; i <= hi; i++) { + temp[i] = nums[i]; + } + + // 进行效率优化,维护左闭右开区间 [mid+1, end) 中的元素乘 2 小于 nums[i] + // 为什么 end 是开区间?因为这样的话可以保证初始区间 [mid+1, mid+1) 是一个空区间 + int end = mid + 1; + for (int i = lo; i <= mid; i++) { + // nums 中的元素可能较大,乘 2 可能溢出,所以转化成 long + while (end <= hi && nums[i] > nums[end]) { + end++; + } + count += end - (mid + 1); + } + + // 数组双指针技巧,合并两个有序数组 + int i = lo, j = mid + 1; + for (int p = lo; p <= hi; p++) { + if (i == mid + 1) { + nums[p] = temp[j++]; + } else if (j == hi + 1) { + nums[p] = temp[i++]; + } else if (temp[i] > temp[j]) { + nums[p] = temp[j++]; + } else { + nums[p] = temp[i++]; + } + } + } + +private: + vector temp; +}; +``` + +```go +// by chatGPT (go) +func reversePairs(nums []int) int { + if len(nums) == 0 { + return 0 + } + // 执行归并排序并统计「翻转对」的个数 + return sort(nums) +} + +func sort(nums []int) int { + var temp = make([]int, len(nums)) + var count = 0 // 统计翻转对个数 + sortHelper(nums, 0, len(nums) - 1, temp, &count) + return count +} + +// 归并排序 +func sortHelper(nums []int, lo, hi int, temp []int, count *int) { + if lo == hi { + return + } + mid := lo + (hi - lo) / 2 + sortHelper(nums, lo, mid, temp, count) + sortHelper(nums, mid + 1, hi, temp, count) + merge(nums, lo, mid, hi, temp, count) +} + +func merge(nums []int, lo, mid, hi int, temp []int, count *int) { + // 先将 nums 中 [lo, hi] 的数复制到 temp 中 + for i := lo; i <= hi; i++ { + temp[i] = nums[i] + } + + // 进行效率优化,维护左闭右开区间 [mid+1, end) 中的元素乘 2 小于 nums[i] + // 为什么 end 是开区间?因为这样的话可以保证初始区间 [mid+1, mid+1) 是一个空区间 + end := mid + 1 + for i := lo; i <= mid; i++ { + // nums 中的元素可能较大,乘 2 可能溢出,所以转化成 int64 + for end <= hi && int64(nums[i]) > int64(nums[end]) * 2 { + end++ + } + *count += end - (mid + 1) + } + + // 数组双指针技巧,合并两个有序数组 + i, j := lo, mid + 1 + for p := lo; p <= hi; p++ { + if i == mid + 1 { + nums[p] = temp[j] + j++ + } else if j == hi + 1 { + nums[p] = temp[i] + i++ + } else if temp[i] > temp[j] { + nums[p] = temp[j] + j++ + } else { + nums[p] = temp[i] + i++ + } + } +} +``` + +```java +// by labuladong (java) +class Solution { + public int reversePairs(int[] nums) { + if (nums.length == 0) { + return 0; + } + // 执行归并排序 + sort(nums); + return count; + } + + private int[] temp; + + public void sort(int[] nums) { + temp = new int[nums.length]; + sort(nums, 0, nums.length - 1); + } + + // 归并排序 + private void sort(int[] nums, int lo, int hi) { + if (lo == hi) { + return; + } + int mid = lo + (hi - lo) / 2; + sort(nums, lo, mid); + sort(nums, mid + 1, hi); + merge(nums, lo, mid, hi); + } + + // 记录「翻转对」的个数 + private int count = 0; + + private void merge(int[] nums, int lo, int mid, int hi) { + for (int i = lo; i <= hi; i++) { + temp[i] = nums[i]; + } + + // 进行效率优化,维护左闭右开区间 [mid+1, end) 中的元素乘 2 小于 nums[i] + // 为什么 end 是开区间?因为这样的话可以保证初始区间 [mid+1, mid+1) 是一个空区间 + int end = mid + 1; + for (int i = lo; i <= mid; i++) { + // nums 中的元素可能较大,乘 2 可能溢出,所以转化成 long + while (end <= hi && nums[i] > nums[end]) { + end++; + } + count += end - (mid + 1); + } + + // 数组双指针技巧,合并两个有序数组 + int i = lo, j = mid + 1; + for (int p = lo; p <= hi; p++) { + if (i == mid + 1) { + nums[p] = temp[j++]; + } else if (j == hi + 1) { + nums[p] = temp[i++]; + } else if (temp[i] > temp[j]) { + nums[p] = temp[j++]; + } else { + nums[p] = temp[i++]; + } + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var reversePairs = function(nums) { + if (nums.length == 0) { + return 0; + } + // 执行归并排序 + sort(nums); + return count; +}; + +function sort(nums) { + var temp = new Array(nums.length); + var count = new Array(1); + sortUtil(nums, 0, nums.length - 1, temp, count); +} + +// 归并排序 +function sortUtil(nums, lo, hi, temp, count) { + if (lo == hi) { + return; + } + var mid = parseInt(lo + (hi - lo) / 2); + sortUtil(nums, lo, mid, temp, count); + sortUtil(nums, mid + 1, hi, temp, count); + merge(nums, lo, mid, hi, temp, count); +} + +// 记录「翻转对」的个数 +function merge(nums, lo, mid, hi, temp, count) { + for (var i = lo; i <= hi; i++) { + temp[i] = nums[i]; + } + + // 进行效率优化,维护左闭右开区间 [mid+1, end) 中的元素乘 2 小于 nums[i] + // 为什么 end 是开区间?因为这样的话可以保证初始区间 [mid+1, mid+1) 是一个空区间 + var end = mid + 1; + for (var i = lo; i <= mid; i++) { + // nums 中的元素可能较大,乘 2 可能溢出,所以转化成 long + while (end <= hi && nums[i] > nums[end]) { + end++; + } + count[0] += end - (mid + 1); + } + + // 数组双指针技巧,合并两个有序数组 + var i = lo, j = mid + 1; + for (var p = lo; p <= hi; p++) { + if (i == mid + 1) { + nums[p] = temp[j++]; + } else if (j == hi + 1) { + nums[p] = temp[i++]; + } else if (temp[i] > temp[j]) { + nums[p] = temp[j++]; + } else { + nums[p] = temp[i++]; + } + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.count = 0 + self.tmp = [] + + # 包装方法 + def reversePairs(self, nums: List[int]) -> int: + if not nums: + return 0 + # 执行归并排序 + self.ms(nums, 0, len(nums) - 1) + return self.count + + # 归并排序 + def ms(self, nums, left, right): + if left >= right: + return + mid = (left + right) >> 1 + self.ms(nums, left, mid) + self.ms(nums, mid + 1, right) + self.merge(nums, left, mid, right) + + # 合并两个有序数组 + def merge(self, nums, left, mid, right): + i, j = left, mid + 1 + t = [] + # 进行效率优化,维护左闭右开区间 [mid+1, end) 中的元素乘 2 小于 nums[i] + # 为什么 end 是开区间?因为这样的话可以保证初始区间 [mid+1, mid+1) 是一个空区间 + end = mid + 1 + while i <= mid and j <= right: + if nums[i] <= nums[j]: + t.append(nums[i]) + i += 1 + else: + t.append(nums[j]) + j += 1 + self.count += (mid - i + 1) + + while i <= mid: + t.append(nums[i]) + i += 1 + + while j <= right: + t.append(nums[j]) + j += 1 + + nums[left:right + 1] = t +``` + +https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof 的多语言解法👆 + +https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findRepeatNumber(vector& nums) { + for (int num : nums) { + if (nums[abs(num)] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + return abs(num); + } else { + // 把索引 num 的元素置为负数 + nums[abs(num)] *= -1; + } + } + // 如果没有在 for 循环中返回,说明重复的那个元素是 0 + return 0; + } +}; +``` + +```go +// by chatGPT (go) +func findRepeatNumber(nums []int) int { + for _, num := range nums { + if nums[abs(num)] < 0 { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + return abs(num) + } else { + // 把索引 num 的元素置为负数 + nums[abs(num)] *= -1 + } + } + // 如果没有在 for 循环中返回,说明重复的那个元素是 0 + return 0 +} + +func abs(num int) int { + if num < 0 { + return -num + } + return num +} +``` + +```java +// by labuladong (java) +class Solution { + public int findRepeatNumber(int[] nums) { + // 先把 nums 数组中的所有元素都加一,避免 0 的影响 + for (int i = 0; i < nums.length; i++) { + nums[i] = nums[i] + 1; + } + + for (int num : nums) { + // 该元素对应的索引 + int index = Math.abs(num) - 1; + if (nums[index] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + // 注意结果要减一 + return Math.abs(num) - 1; + } else { + // 把索引 num 的元素置为负数 + nums[index] *= -1; + } + } + return -1; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number} + */ +var findRepeatNumber = function(nums) { + for (let num of nums) { + if (nums[Math.abs(num)] < 0) { + // 之前已经把对应索引的元素变成负数了, + // 这说明 num 重复出现了两次 + return Math.abs(num); + } else { + // 把索引 num 的元素置为负数 + nums[Math.abs(num)] *= -1; + } + } + // 如果没有在 for 循环中返回,说明重复的那个元素是 0 + return 0; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findRepeatNumber(self, nums: List[int]) -> int: + for num in nums: + if nums[abs(num)] < 0: + # 之前已经把对应索引的元素变成负数了, + # 这说明 num 重复出现了两次 + return abs(num) + else: + # 把索引 num 的元素置为负数 + nums[abs(num)] *= -1 + # 如果没有在 for 循环中返回,说明重复的那个元素是 0 + return 0 +``` + +https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/simplify-path 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string simplifyPath(string path) { + vector parts; + int start = 0; + for (int i = 1; i <= path.size(); ++i) { + if (path[i] == '/' || i == path.size()) { + string part = path.substr(start + 1, i - start - 1); + if (part == "" || part == ".") { + // part 为空或为 . ,忽略 + } else if (part == "..") { + if (!parts.empty()) parts.pop_back(); + } else { + parts.push_back(part); + } + start = i; + } + } + string res = ""; + for (string part : parts) { + res += "/" + part; + } + return res.empty() ? "/" : res; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "strings" +) + +func simplifyPath(path string) string { + parts := strings.Split(path, "/") + stk := make([]string, 0) + // 借助栈计算最终的文件夹路径 + for _, part := range parts { + if part == "" || part == "." { + continue + } + if part == ".." { + if len(stk) != 0 { + stk = stk[:len(stk)-1] + } + } else { + stk = append(stk, part) + } + } + // 栈中存储的文件夹组成路径 + res := "" + for i := len(stk) - 1; i >= 0; i-- { + res = "/" + stk[i] + res + } + if res == "" { + res = "/" + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public String simplifyPath(String path) { + String[] parts = path.split("/"); + Stack stk = new Stack<>(); + // 借助栈计算最终的文件夹路径 + for (String part : parts) { + if (part.isEmpty() || part.equals(".")) { + continue; + } + if (part.equals("..")) { + if (!stk.isEmpty()) stk.pop(); + continue; + } + stk.push(part); + } + // 栈中存储的文件夹组成路径 + String res = ""; + while (!stk.isEmpty()) { + res = "/" + stk.pop() + res; + } + return res.isEmpty() ? "/" : res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var simplifyPath = function(path) { + var parts = path.split("/"); + var stk = []; + // 借助栈计算最终的文件夹路径 + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + if (part === "" || part === ".") { + continue; + } + if (part === "..") { + if (stk.length !== 0) stk.pop(); + continue; + } + stk.push(part); + } + // 栈中存储的文件夹组成路径 + var res = ""; + while (stk.length !== 0) { + res = "/" + stk.pop() + res; + } + return res === "" ? "/" : res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def simplifyPath(self, path: str) -> str: + parts = path.split("/") + stk = [] + # 借助栈计算最终的文件夹路径 + for part in parts: + if part == '' or part == '.': + continue + if part == '..': + if stk: + stk.pop() + continue + stk.append(part) + # 栈中存储的文件夹组成路径 + res = "" + while stk: + res = '/' + stk.pop() + res + return res if res else '/' +``` + +https://leetcode.cn/problems/simplify-path 的多语言解法👆 + +https://leetcode.cn/problems/single-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int singleNumber(vector& nums) { + int res = 0; + for (int n : nums) { + res ^= n; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +/** + * @param nums: List[int] + * @return: int + */ +func singleNumber(nums []int) int { + res := 0 + for _, n := range nums { + res ^= n + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int singleNumber(int[] nums) { + int res = 0; + for (int n : nums) { + res ^= n; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number} + */ +var singleNumber = function(nums) { + let res = 0; + for (let n of nums) { + res ^= n; + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def singleNumber(self, nums: List[int]) -> int: + res = 0 + for n in nums: + res ^= n + return res +``` + +https://leetcode.cn/problems/single-number 的多语言解法👆 + +https://leetcode.cn/problems/single-threaded-cpu 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector getOrder(vector>& tasks) { + int n = tasks.size(); + // 把原始索引也添加上,方便后面排序用 + vector> triples; + for (int i = 0; i < n; i++) { + triples.push_back({tasks[i][0], tasks[i][1], i}); + } + // 数组先按照任务的开始时间排序 + sort(triples.begin(), triples.end(), [](auto a, auto b){ + return a[0] < b[0]; + }); + + // 按照任务的处理时间排序,如果处理时间相同,按照原始索引排序 + auto cmp = [](vector& a, vector& b){ + if (a[1] != b[1]) { + // 比较处理时间 + return a[1] > b[1]; + } + // 比较原始索引 + return a[2] > b[2]; + }; + priority_queue, vector>, decltype(cmp)> pq(cmp); + + vector res; + // 记录完成任务的时间线 + int now = 0; + int i = 0; + while (res.size() < n) { + if (!pq.empty()) { + // 完成队列中的一个任务 + vector triple = pq.top(); + pq.pop(); + res.push_back(triple[2]); + // 每完成一个任务,就要推进时间线 + now += triple[1]; + } else if (i < n && triples[i][0] > now) { + // 队列为空可能因为还没到开始时间, + // 直接把时间线推进到最近任务的开始时间 + now = triples[i][0]; + } + + // 由于时间线的推进,会产生可以开始执行的任务 + for (; i < n && triples[i][0] <= now; i++) { + pq.push(triples[i]); + } + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +// 定义任务 +type task struct { + // 入队时间,即开始时间 + enqueTime int + // 处理时间 + interval int + // 原始索引 + id int +} + +// 自定义优先队列,按照任务处理时间排序,如果处理时间相同,按照原始索引排序 +type queue []task + +// 比较器,如果想要当前元素在队首,那么返回 true +func (q queue) Less(i, j int) bool { + if q[i].interval == q[j].interval { + return q[i].id < q[j].id + } + return q[i].interval < q[j].interval +} + +// 计算队列长度 +func (q queue) Len() int { + return len(q) +} + +// 交换元素 +func (q queue) Swap(i, j int) { + q[i], q[j] = q[j], q[i] +} + +// 入队 +func (q *queue) Push(p interface{}) { + *q = append(*q, p.(task)) +} + +// 出队 +func (q *queue) Pop() interface{} { + top := (*q)[len(*q)-1] + *q = (*q)[:len(*q)-1] + return top +} + +func getOrder(tasks [][]int) []int { + // 任务数量 + n := len(tasks) + // 把原始索引也添加上,方便后面排序用 + tasksWithID := make([]task, n) + for i := range tasks { + tasksWithID[i] = task{tasks[i][0], tasks[i][1], i} + } + // 数组先按照任务的开始时间排序 + sort.Slice(tasksWithID, func(i, j int) bool { + return tasksWithID[i].enqueTime < tasksWithID[j].enqueTime + }) + + // 任务队列 + q := make(queue, 0, n) + // 结果数组 + res := make([]int, 0, n) + // 记录现在的时间线 + i, now := 0, 0 + // 当前还未完成的任务数目 + for len(res) < n { + // 时间线被推进,产生新任务 + for ; i < n && tasksWithID[i].enqueTime <= now; i++ { + q = append(q, tasksWithID[i]) + } + // 如果当前队列为空,因为时间线还未到第一个任务的开始时间,直接将时间线推进到第一个任务的开始时间,并将 i 移向下一个任务 + if len(q) == 0 { + now = tasksWithID[i].enqueTime + i++ + } else { // 处理队列中任务 + heap.Init(&q) + top := heap.Pop(&q).(task) + res = append(res, top.id) + now += top.interval + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] getOrder(int[][] tasks) { + int n = tasks.length; + // 把原始索引也添加上,方便后面排序用 + ArrayList triples = new ArrayList<>(); + for (int i = 0; i < tasks.length; i++) { + triples.add(new int[]{tasks[i][0], tasks[i][1], i}); + } + // 数组先按照任务的开始时间排序 + triples.sort((a, b) -> { + return a[0] - b[0]; + }); + + // 按照任务的处理时间排序,如果处理时间相同,按照原始索引排序 + PriorityQueue pq = new PriorityQueue<>((a, b) -> { + if (a[1] != b[1]) { + // 比较处理时间 + return a[1] - b[1]; + } + // 比较原始索引 + return a[2] - b[2]; + }); + + ArrayList res = new ArrayList<>(); + // 记录完成任务的时间线 + int now = 0; + int i = 0; + while (res.size() < n) { + if (!pq.isEmpty()) { + // 完成队列中的一个任务 + int[] triple = pq.poll(); + res.add(triple[2]); + // 每完成一个任务,就要推进时间线 + now += triple[1]; + } else if (i < n && triples.get(i)[0] > now) { + // 队列为空可能因为还没到开始时间, + // 直接把时间线推进到最近任务的开始时间 + now = triples.get(i)[0]; + } + + // 由于时间线的推进,会产生可以开始执行的任务 + for (; i < n && triples.get(i)[0] <= now; i++) { + pq.offer(triples.get(i)); + } + } + + // Java 语言特性,将 List 转化成 int[] 格式 + int[] arr = new int[n]; + for (int j = 0; j < n; j++) { + arr[j] = res.get(j); + } + return arr; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} tasks + * @return {number[]} + */ +var getOrder = function(tasks) { + const n = tasks.length; + + // 把原始索引也添加上,方便后面排序用 + const triples = []; + for (let i = 0; i < tasks.length; i++) { + triples.push([tasks[i][0], tasks[i][1], i]); + } + + // 数组先按照任务的开始时间排序 + triples.sort((a, b) => { + return a[0] - b[0]; + }); + + // 按照任务的处理时间排序,如果处理时间相同,按照原始索引排序 + const pq = new PriorityQueue((a, b) => { + if (a[1] !== b[1]) { + // 比较处理时间 + return a[1] - b[1]; + } + // 比较原始索引 + return a[2] - b[2]; + }); + + const res = []; + // 记录完成任务的时间线 + let now = 0; + let i = 0; + while (res.length < n) { + if (!pq.isEmpty()) { + // 完成队列中的一个任务 + const triple = pq.poll(); + res.push(triple[2]); + // 每完成一个任务,就要推进时间线 + now += triple[1]; + } else if (i < n && triples[i][0] > now) { + // 队列为空可能因为还没到开始时间, + // 直接把时间线推进到最近任务的开始时间 + now = triples[i][0]; + } + + // 由于时间线的推进,会产生可以开始执行的任务 + for (; i < n && triples[i][0] <= now; i++) { + pq.offer(triples[i]); + } + } + + // JavaScript语言特性,不需要转化成int[]格式 + return res; +}; + +class PriorityQueue { + constructor(comparator) { + this.heap = []; + this.comparator = comparator; + } + + isEmpty() { + return this.heap.length === 0; + } + + size() { + return this.heap.length; + } + + peek() { + return this.isEmpty() ? null : this.heap[0]; + } + + offer(item) { + this.heap.push(item); + this.siftUp(this.heap.length - 1); + } + + poll() { + if (this.isEmpty()) { + return null; + } + + const item = this.heap[0]; + this.heap[0] = this.heap[this.heap.length - 1]; + this.heap.pop(); + this.siftDown(0); + return item; + } + + heapify() { + if (this.isEmpty()) { + return; + } + + for (let i = Math.floor(this.size() / 2) - 1; i >= 0; i--) { + this.siftDown(i); + } + } + + siftUp(index) { + let parent = Math.floor((index - 1) / 2); + + while (index > 0 && this.comparator(this.heap[index], this.heap[parent]) < 0) { + const tmp = this.heap[index]; + this.heap[index] = this.heap[parent]; + this.heap[parent] = tmp; + + index = parent; + parent = Math.floor((index - 1) / 2); + } + } + + siftDown(index) { + let child = index * 2 + 1; + + while (child < this.size()) { + if (child + 1 < this.size() && this.comparator(this.heap[child + 1], this.heap[child]) < 0) { + child++; + } + + if (this.comparator(this.heap[child], this.heap[index]) < 0) { + const tmp = this.heap[index]; + this.heap[index] = this.heap[child]; + this.heap[child] = tmp; + + index = child; + child = index * 2 + 1; + } else { + break; + } + } + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def getOrder(self, tasks: List[List[int]]) -> List[int]: + n = len(tasks) + # 把原始索引也添加上,方便后面排序用 + triples = [] + for i in range(n): + triples.append([tasks[i][0], tasks[i][1], i]) + # 数组先按照任务的开始时间排序 + triples.sort(key=lambda a: a[0]) + + # 按照任务的处理时间排序,如果处理时间相同,按照原始索引排序 + pq = [] + heapq.heapify(pq) + for a in triples: + heapq.heappush(pq, (a[1], a[2])) + + res = [] + # 记录完成任务的时间线 + now = 0 + while len(res) < n: + if pq: + # 完成队列中的一个任务 + triple = heapq.heappop(pq) + res.append(triple[1]) + # 每完成一个任务,就要推进时间线 + now += triple[0] + elif triples[0][0] > now: + # 队列为空可能因为还没到开始时间, + # 直接把时间线推进到最近任务的开始时间 + now = triples[0][0] + else: + # 由于时间线的推进,会产生可以开始执行的任务 + while triples and triples[0][0] <= now: + a = triples.pop(0) + heapq.heappush(pq, (a[1], a[2])) + + # Java 语言特性,将 List 转化成 int[] 格式 + return res +``` + +https://leetcode.cn/problems/single-threaded-cpu 的多语言解法👆 + +https://leetcode.cn/problems/sliding-puzzle 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int slidingPuzzle(vector>& board) { + int m = 2, n = 3; + string target = "123450"; + // 将 2x3 的数组转化成字符串作为 BFS 的起点 + string start = ""; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + start += to_string(board[i][j]); + } + } + + // 记录一维字符串的相邻索引 + int neighbor[6][3] = { + {1, 3}, + {0, 4, 2}, + {1, 5}, + {0, 4}, + {3, 1, 5}, + {4, 2} + }; + + /******* BFS 算法框架开始 *******/ + queue q; + unordered_set visited; + // 从起点开始 BFS 搜索 + q.push(start); + visited.insert(start); + + int step = 0; + while (!q.empty()) { + int sz = q.size(); + for (int i = 0; i < sz; i++) { + string cur = q.front(); + q.pop(); + // 判断是否达到目标局面 + if (target == cur) { + return step; + } + // 找到数字 0 的索引 + int idx = 0; + for (; cur[idx] != '0'; idx++) ; + // 将数字 0 和相邻的数字交换位置 + for (int j = 0; j < 3; j++) { + int adj = neighbor[idx][j]; + string new_board = swap(cur, adj, idx); + // 防止走回头路 + if (visited.find(new_board) == visited.end()) { + q.push(new_board); + visited.insert(new_board); + } + } + } + step++; + } + /******* BFS 算法框架结束 *******/ + return -1; + } + + string swap(string str, int i, int j) { + char temp = str[i]; + str[i] = str[j]; + str[j] = temp; + return str; + } +}; +``` + +```go +// by chatGPT (go) +import "fmt" + +func slidingPuzzle(board [][]int) int { + m, n := 2, 3 + sb := make([]byte, 0, 6) + target := "123450" + // 将 2x3 的数组转化成字符串作为 BFS 的起点 + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + sb = append(sb, byte(board[i][j]+'0')) + } + } + start := string(sb) + + // 记录一维字符串的相邻索引 + neighbor := [][]int{ + {1, 3}, + {0, 4, 2}, + {1, 5}, + {0, 4}, + {3, 1, 5}, + {4, 2}, + } + + /******* BFS 算法框架开始 *******/ + q := []string{start} + visited := make(map[string]bool) + // 从起点开始 BFS 搜索 + visited[start] = true + + step := 0 + for len(q) > 0 { + sz := len(q) + for i := 0; i < sz; i++ { + cur := q[i] + // 判断是否达到目标局面 + if cur == target { + return step + } + // 找到数字 0 的索引 + idx := 0 + for cur[idx] != '0' { + idx++ + } + // 将数字 0 和相邻的数字交换位置 + for _, adj := range neighbor[idx] { + newBoard := swap([]byte(cur), adj, idx) + // 防止走回头路 + if !visited[newBoard] { + q = append(q, newBoard) + visited[newBoard] = true + } + } + } + q = q[sz:] + step++ + } + /******* BFS 算法框架结束 *******/ + return -1 +} + +func swap(chars []byte, i, j int) string { + temp := chars[i] + chars[i] = chars[j] + chars[j] = temp + return string(chars) +} +``` + +```java +// by labuladong (java) +class Solution { + public int slidingPuzzle(int[][] board) { + int m = 2, n = 3; + StringBuilder sb = new StringBuilder(); + String target = "123450"; + // 将 2x3 的数组转化成字符串作为 BFS 的起点 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + sb.append(board[i][j]); + } + } + String start = sb.toString(); + + // 记录一维字符串的相邻索引 + int[][] neighbor = new int[][]{ + {1, 3}, + {0, 4, 2}, + {1, 5}, + {0, 4}, + {3, 1, 5}, + {4, 2} + }; + + /******* BFS 算法框架开始 *******/ + Queue q = new LinkedList<>(); + HashSet visited = new HashSet<>(); + // 从起点开始 BFS 搜索 + q.offer(start); + visited.add(start); + + int step = 0; + while (!q.isEmpty()) { + int sz = q.size(); + for (int i = 0; i < sz; i++) { + String cur = q.poll(); + // 判断是否达到目标局面 + if (target.equals(cur)) { + return step; + } + // 找到数字 0 的索引 + int idx = 0; + for (; cur.charAt(idx) != '0'; idx++) ; + // 将数字 0 和相邻的数字交换位置 + for (int adj : neighbor[idx]) { + String new_board = swap(cur.toCharArray(), adj, idx); + // 防止走回头路 + if (!visited.contains(new_board)) { + q.offer(new_board); + visited.add(new_board); + } + } + } + step++; + } + /******* BFS 算法框架结束 *******/ + return -1; + } + + private String swap(char[] chars, int i, int j) { + char temp = chars[i]; + chars[i] = chars[j]; + chars[j] = temp; + return new String(chars); + } + +} +``` + +```javascript +// by chatGPT (javascript) +var slidingPuzzle = function(board) { + const m = 2, n = 3; + + let sb = ''; + const target = '123450'; + // 将 2x3 的数组转化成字符串作为 BFS 的起点 + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + sb += board[i][j]; + } + } + let start = sb.toString(); + + // 记录一维字符串的相邻索引 + const neighbor = [ + [1, 3], + [0, 4, 2], + [1, 5], + [0, 4], + [3, 1, 5], + [4, 2] + ]; + + /******* BFS 算法框架开始 *******/ + let q = []; + const visited = new Set(); + // 从起点开始 BFS 搜索 + q.push(start); + visited.add(start); + + let step = 0; + while (q.length) { + const sz = q.length; + for (let i = 0; i < sz; i++) { + const cur = q.shift(); + // 判断是否达到目标局面 + if (target === cur) { + return step; + } + // 找到数字 0 的索引 + let idx = 0; + for (; cur.charAt(idx) !== '0'; idx++) ; + // 将数字 0 和相邻的数字交换位置 + for (const adj of neighbor[idx]) { + const new_board = swap(cur.slice(0), adj, idx); + // 防止走回头路 + if (!visited.has(new_board)) { + q.push(new_board); + visited.add(new_board); + } + } + } + step++; + } + /******* BFS 算法框架结束 *******/ + return -1; +}; + +var swap = function(chars, i, j) { + [chars[i], chars[j]] = [chars[j], chars[i]]; + return chars.join(''); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def slidingPuzzle(self, board: List[List[int]]) -> int: + m, n = 2, 3 + sb = [] + target = "123450" + # 将 2x3 的数组转化成字符串作为 BFS 的起点 + for i in range(m): + for j in range(n): + sb.append(str(board[i][j])) + start = ''.join(sb) + + # 记录一维字符串的相邻索引 + neighbor = [ + [1, 3], + [0, 4, 2], + [1, 5], + [0, 4], + [3, 1, 5], + [4, 2] + ] + + # BFS 算法框架开始 + q = collections.deque() + visited = set() + # 从起点开始 BFS 搜索 + q.append(start) + visited.add(start) + + step = 0 + while q: + sz = len(q) + for i in range(sz): + cur = q.popleft() + # 判断是否达到目标局面 + if target == cur: + return step + # 找到数字 0 的索引 + idx = cur.find('0') + # 将数字 0 和相邻的数字交换位置 + for adj in neighbor[idx]: + new_board = self.swap(cur, adj, idx) + # 防止走回头路 + if new_board not in visited: + q.append(new_board) + visited.add(new_board) + step += 1 + # BFS 算法框架结束 + return -1 + + def swap(self, chars, i, j): + chars = list(chars) + chars[i], chars[j] = chars[j], chars[i] + return ''.join(chars) +``` + +https://leetcode.cn/problems/sliding-puzzle 的多语言解法👆 + +https://leetcode.cn/problems/sliding-window-maximum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + /* 单调队列的实现 */ + class MonotonicQueue { + private: + deque q; + public: + void push(int n) { + // 将小于 n 的元素全部删除 + while (!q.empty() && q.back() < n) { + q.pop_back(); + } + // 然后将 n 加入尾部 + q.push_back(n); + } + + int max() { + return q.front(); + } + + void pop(int n) { + if (n == q.front()) { + q.pop_front(); + } + } + }; + + /* 解题函数的实现 */ + vector maxSlidingWindow(vector& nums, int k) { + MonotonicQueue window; + vector res; + + for (int i = 0; i < nums.size(); i++) { + if (i < k - 1) { + //先填满窗口的前 k - 1 + window.push(nums[i]); + } else { + // 窗口向前滑动,加入新数字 + window.push(nums[i]); + // 记录当前窗口的最大值 + res.push_back(window.max()); + // 移出旧数字 + window.pop(nums[i - k + 1]); + } + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +/* 单调队列的实现 */ +type MonotonicQueue struct { + q []int +} + +func (mq *MonotonicQueue) push(n int) { + // 将小于 n 的元素全部删除 + for len(mq.q) > 0 && mq.q[len(mq.q)-1] < n { + mq.q = mq.q[:len(mq.q)-1] + } + // 然后将 n 加入尾部 + mq.q = append(mq.q, n) +} + +func (mq *MonotonicQueue) max() int { + return mq.q[0] +} + +func (mq *MonotonicQueue) pop(n int) { + if n == mq.q[0] { + mq.q = mq.q[1:] + } +} + +/* 解题函数的实现 */ +func maxSlidingWindow(nums []int, k int) []int { + window := MonotonicQueue{make([]int, 0)} + res := make([]int, 0) + + for i := 0; i < len(nums); i++ { + if i < k-1 { + // 先填满窗口的前 k - 1 + window.push(nums[i]) + } else { + // 窗口向前滑动,加入新数字 + window.push(nums[i]) + // 记录当前窗口的最大值 + res = append(res, window.max()) + // 移出旧数字 + window.pop(nums[i-k+1]) + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + /* 单调队列的实现 */ + class MonotonicQueue { + LinkedList q = new LinkedList<>(); + public void push(int n) { + // 将小于 n 的元素全部删除 + while (!q.isEmpty() && q.getLast() < n) { + /** + ![](../pictures/单调队列/3.png) + */ + q.pollLast(); + } + // 然后将 n 加入尾部 + q.addLast(n); + } + + public int max() { + return q.getFirst(); + } + + public void pop(int n) { + if (n == q.getFirst()) { + q.pollFirst(); + } + } + } + + /* 解题函数的实现 */ + public int[] maxSlidingWindow(int[] nums, int k) { + MonotonicQueue window = new MonotonicQueue(); + List res = new ArrayList<>(); + + for (int i = 0; i < nums.length; i++) { + if (i < k - 1) { + //先填满窗口的前 k - 1 + window.push(nums[i]); + } else { + /** + ![](../pictures/单调队列/1.png) + */ + // 窗口向前滑动,加入新数字 + window.push(nums[i]); + // 记录当前窗口的最大值 + res.add(window.max()); + // 移出旧数字 + window.pop(nums[i - k + 1]); + } + } + // 需要转成 int[] 数组再返回 + int[] arr = new int[res.size()]; + for (int i = 0; i < res.size(); i++) { + arr[i] = res.get(i); + } + return arr; + } +} +``` + +```javascript +// by chatGPT (javascript) +var maxSlidingWindow = function(nums, k) { + /** + * 单调队列的实现 + */ + class MonotonicQueue { + constructor() { + this.q = [] + } + + push(n) { + // 将小于 n 的元素全部删除 + while (this.q.length !== 0 && this.q[this.q.length - 1] < n) { + /** + ![](../pictures/单调队列/3.png) + */ + this.q.pop() + } + // 然后将 n 加入尾部 + this.q.push(n) + } + + max() { + return this.q[0] + } + + pop(n) { + if (this.q[0] === n) { + this.q.shift() + } + } + } + + /** + * 解题函数的实现 + */ + const window = new MonotonicQueue() + const res = [] + + for (let i = 0; i < nums.length; i++) { + if (i < k - 1) { + //先填满窗口的前 k - 1 + window.push(nums[i]) + } else { + /** + ![](../pictures/单调队列/1.png) + */ + // 窗口向前滑动,加入新数字 + window.push(nums[i]) + // 记录当前窗口的最大值 + res.push(window.max()) + // 移出旧数字 + window.pop(nums[i - k + 1]) + } + } + return res +} +``` + +```python +# by chatGPT (python) +class Solution: + # Monotonic Queue Implementation + class MonotonicQueue: + def __init__(self): + self.q = [] + + # Push elements into the queue + def push(self, n): + # remove all elements smaller than n from the tail of the queue + while self.q and self.q[-1] < n: + # extend down -300 + # ![](../pictures/单调队列/3.png) + self.q.pop() + # then insert n at the tail + self.q.append(n) + + # Get maximum element in the queue + def max(self): + return self.q[0] + + # Remove element n from the queue + def pop(self, n): + if n == self.q[0]: + self.q.pop(0) + + # Implementing the solution function + def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: + window = self.MonotonicQueue() + res = [] + + for i in range(len(nums)): + if i < k - 1: + # fill up the first k-1 elements of the window initially + window.push(nums[i]) + else: + # slide the window forward and add the new element + window.push(nums[i]) + # record the maximum value of the current window + res.append(window.max()) + # remove the old element + window.pop(nums[i - k + 1]) + + # converting the recorded elements into an integer array + return res +``` + +https://leetcode.cn/problems/sliding-window-maximum 的多语言解法👆 + +https://leetcode.cn/problems/smallest-subsequence-of-distinct-characters 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string smallestSubsequence(string s) { + stack stk; + + // 维护一个计数器记录字符串中字符的数量 + // 因为输入为 ASCII 字符,大小 256 够用了 + int count[256] = {0}; + for (int i = 0; i < s.length(); i++) { + count[s[i]]++; + } + + bool inStack[256] = {false}; + for (char c : s) { + // 每遍历过一个字符,都将对应的计数减一 + count[c]--; + + if (inStack[c]) continue; + + while (!stk.empty() && stk.top() > c) { + // 若之后不存在栈顶元素了,则停止 pop + if (count[stk.top()] == 0) { + break; + } + // 若之后还有,则可以 pop + inStack[stk.top()] = false; + stk.pop(); + } + stk.push(c); + inStack[c] = true; + } + + string res; + while (!stk.empty()) { + res += stk.top(); + stk.pop(); + } + reverse(res.begin(), res.end()); + return res; + } +}; +``` + +```go +// by chatGPT (go) +func smallestSubsequence(s string) string { + stk := make([]byte, 0) + + // 维护一个计数器记录字符串中字符的数量 + // 因为输入为 ASCII 字符,大小 256 够用了 + count := make([]int, 256) + for i := 0; i < len(s); i++ { + count[s[i]]++ + } + + inStack := make([]bool, 256) + for _, c := range s { + // 每遍历过一个字符,都将对应的计数减一 + count[c]-- + + if inStack[c] { + continue + } + + for len(stk) > 0 && stk[len(stk)-1] > byte(c) { + // 若之后不存在栈顶元素了,则停止 pop + if count[stk[len(stk)-1]] == 0 { + break + } + // 若之后还有,则可以 pop + inStack[stk[len(stk)-1]] = false + stk = stk[:len(stk)-1] + } + stk = append(stk, byte(c)) + inStack[c] = true + } + + sb := make([]byte, 0, len(s)) + for i := len(stk) - 1; i >= 0; i-- { + sb = append(sb, stk[i]) + } + return string(sb) +} +``` + +```java +// by labuladong (java) +class Solution { + public String smallestSubsequence(String s) { + Stack 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(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @return {string} + */ +var smallestSubsequence = function(s) { + // 创建一个“栈” data structure(使用 array 实现)来处理字符串 + const stk = []; + // 创建一个数组用于记录字符数量 + // 因为输入为 ASCII 字符,大小 256 够用了 + const count = new Array(256).fill(0); + for(let i = 0; i < s.length; i++) { + count[s.charCodeAt(i)]++; + } + + // 创建一个 boolean 数组来记录字符是否出现在“栈”中 + const inStack = new Array(256).fill(false); + for(let c of s) { + // 每遍历过一个字符,都将对应的计数减一 + count[c.charCodeAt()]--; + // 如果该字符已经在“栈”中,则跳过 + if(inStack[c.charCodeAt()]) continue; + // 如果该字符不在“栈”中,则需要将“栈”中比该字符大的字符出栈 + while(stk.length && stk[stk.length - 1] > c) { + // 如果弹出的字符在后面还会出现,则需要添加回来,停止出栈 + if(count[stk[stk.length - 1].charCodeAt()] === 0) break; + // 如果弹出的字符在后面不再出现,则可以继续出栈 + inStack[stk.pop().charCodeAt()] = false; + } + // 将字符压入“栈”中,标记为出现 + stk.push(c); + inStack[c.charCodeAt()] = true; + } + // 构造结果字符串 + return stk.join(''); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def smallestSubsequence(self, s: str) -> str: + stk = [] + # 维护一个计数器记录字符串中字符的数量 + # 因为输入为 ASCII 字符,大小 256 够用了 + count = [0] * 256 + for i in range(len(s)): + count[ord(s[i])] += 1 + + inStack = [False] * 256 + for c in s: + # 每遍历过一个字符,都将对应的计数减一 + count[ord(c)] -= 1 + + if inStack[ord(c)]: + continue + + while stk and stk[-1] > c: + # 若之后不存在栈顶元素了,则停止 pop + if count[ord(stk[-1])] == 0: + break + # 若之后还有,则可以 pop + inStack[ord(stk.pop())] = False + + stk.append(c) + inStack[ord(c)] = True + + return ''.join(stk) +``` + +https://leetcode.cn/problems/smallest-subsequence-of-distinct-characters 的多语言解法👆 + +https://leetcode.cn/problems/sort-characters-by-frequency 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string frequencySort(string s) { + vector chars(s.begin(), s.end()); + // s 中的字符 -> 该字符出现的频率 + unordered_map charToFreq; + for (char ch : chars) { + charToFreq[ch]++; + } + + auto cmp = [](pair& entry1, pair& entry2) { + return entry1.second < entry2.second; + }; + // 队列按照键值对中的值(字符出现频率)从大到小排序 + priority_queue, vector>, decltype(cmp)> pq(cmp); + + // 按照字符频率排序 + for (auto& entry : charToFreq) { + pq.push(entry); + } + + string res = ""; + while (!pq.empty()) { + // 把频率最高的字符排在前面 + pair entry = pq.top(); + pq.pop(); + res += string(entry.second, entry.first); + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +// 题目地址 https://leetcode-cn.com/problems/sort-characters-by-frequency/ +func frequencySort(s string) string { + chars := []rune(s) + // s 中的字符 -> 该字符出现的频率 + charToFreq := make(map[rune]int) + for _, ch := range chars { + charToFreq[ch]++ + } + + pq := priorityQueue{} + // 队列按照键值对中的值(字符出现频率)从大到小排序 + for k, v := range charToFreq { + pq = append(pq, entry{k, v}) + } + sort.Sort(sort.Reverse(pq)) + + var sb strings.Builder + for _, e := range pq { + // 把频率最高的字符排在前面 + s := strings.Repeat(string(e.ch), e.count) + sb.WriteString(s) + } + + return sb.String() +} + +type entry struct { + ch rune + count int +} + +type priorityQueue []entry + +func (pq priorityQueue) Len() int { return len(pq) } +func (pq priorityQueue) Less(i, j int) bool { return pq[i].count < pq[j].count } +func (pq priorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] } +func (pq *priorityQueue) Push(x interface{}) { + *pq = append(*pq, x.(entry)) +} +func (pq *priorityQueue) Pop() interface{} { + old := *pq + n := len(old) + x := old[n-1] + *pq = old[0 : n-1] + return x +} +``` + +```java +// by labuladong (java) +class Solution { + public String frequencySort(String s) { + char[] chars = s.toCharArray(); + // s 中的字符 -> 该字符出现的频率 + HashMap charToFreq = new HashMap<>(); + for (char ch : chars) { + charToFreq.put(ch, charToFreq.getOrDefault(ch, 0) + 1); + } + + PriorityQueue> + pq = new PriorityQueue<>((entry1, entry2) -> { + // 队列按照键值对中的值(字符出现频率)从大到小排序 + return entry2.getValue().compareTo(entry1.getValue()); + }); + + // 按照字符频率排序 + for (Map.Entry entry : charToFreq.entrySet()) { + pq.offer(entry); + } + + StringBuilder sb = new StringBuilder(); + while (!pq.isEmpty()) { + // 把频率最高的字符排在前面 + Map.Entry entry = pq.poll(); + String part = String.valueOf(entry.getKey()).repeat(entry.getValue()); + sb.append(part); + } + + return sb.toString(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** +* @param {string} s +* @return {string} +*/ +var frequencySort = function(s) { + let chars = s.split(''); + let charToFreq = new Map(); + chars.forEach(ch => { + charToFreq.set(ch, charToFreq.get(ch) + 1 || 1); + }); + + let pq = new PriorityQueue((a, b) => { + // 队列按照键值对中的值(字符出现频率)从大到小排序 + return b.value - a.value; + }); + + // 按照字符频率排序 + charToFreq.forEach((value, key) => { + pq.offer({key, value}); + }); + + let sb = []; + while (!pq.isEmpty()) { + // 把频率最高的字符排在前面 + let {key, value} = pq.poll(); + let part = key.repeat(value); + sb.push(part); + } + + return sb.join(''); +}; + +class PriorityQueue { + constructor(compareFn) { + this.queue = []; + this.compareFn = compareFn; + } + + isEmpty() { + return this.queue.length === 0; + } + + offer(item) { + this.queue.push(item); + this.queue.sort(this.compareFn); + } + + poll() { + return this.queue.shift(); + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def frequencySort(self, s: str) -> str: + chars = list(s) + # s 中的字符 -> 该字符出现的频率 + charToFreq = {} + for ch in chars: + charToFreq[ch] = charToFreq.get(ch, 0) + 1 + + pq = [] + for key, value in charToFreq.items(): + # 创建优先队列 + pq.append((value, key)) + + # 对优先队列中的元素按照出现频率进行排序 + pq.sort(reverse=True) + + res = '' + for freq, ch in pq: + # 把频率最高的字符排在前面 + part = ch * freq + res += part + + return res +``` + +https://leetcode.cn/problems/sort-characters-by-frequency 的多语言解法👆 + +https://leetcode.cn/problems/sort-the-matrix-diagonally 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> diagonalSort(vector>& mat) { + int m = mat.size(), n = mat[0].size(); + + // 存储所有对角线的元素列表 + unordered_map> diagonals; + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 横纵坐标之差可以作为一条对角线的 ID + int diagonalID = i - j; + diagonals[diagonalID].push_back(mat[i][j]); + } + } + + // 从数组末尾删除元素效率较高,所以我们把 vector 倒序排序 + for (auto& diagonal: diagonals) { + sort(diagonal.second.rbegin(), diagonal.second.rend()); + } + + // 把排序结果回填二维矩阵 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + vector& diagonal = diagonals[i - j]; + mat[i][j] = diagonal.back(); + diagonal.pop_back(); + } + } + + return mat; + } +}; +``` + +```go +// by chatGPT (go) +// DiagonalSort function +func DiagonalSort(mat [][]int) [][]int { + m, n := len(mat), len(mat[0]) + + // 存储所有对角线的元素列表 + diagonals := make(map[int][]int) + + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + // 横纵坐标之差可以作为一条对角线的 ID + diagonalID := i - j + if _, ok := diagonals[diagonalID]; !ok { + diagonals[diagonalID] = make([]int, 0) + } + diagonals[diagonalID] = append(diagonals[diagonalID], mat[i][j]) + } + } + + // 从数组末尾删除元素效率较高,所以我们把 []int 倒序排序 + for _, v := range diagonals { + sort.Sort(sort.Reverse(sort.IntSlice(v))) + } + + // 把排序结果回填二维矩阵 + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + diagonal := diagonals[i-j] + mat[i][j] = diagonal[len(diagonal)-1] + diagonals[i-j] = diagonal[:len(diagonal)-1] + } + } + + return mat +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] diagonalSort(int[][] mat) { + int m = mat.length, n = mat[0].length; + + // 存储所有对角线的元素列表 + HashMap> diagonals = new HashMap<>(); + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 横纵坐标之差可以作为一条对角线的 ID + int diagonalID = i - j; + diagonals.putIfAbsent(diagonalID, new ArrayList<>()); + diagonals.get(diagonalID).add(mat[i][j]); + } + } + + // 从数组末尾删除元素效率较高,所以我们把 ArrayList 倒序排序 + for (List diagonal: diagonals.values()) { + Collections.sort(diagonal, Collections.reverseOrder()); + } + + // 把排序结果回填二维矩阵 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + ArrayList diagonal = diagonals.get(i - j); + mat[i][j] = diagonal.remove(diagonal.size() - 1); + } + } + + return mat; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} mat + * @return {number[][]} + */ +var diagonalSort = function(mat) { + let m = mat.length, n = mat[0].length; + + // 存储所有对角线的元素列表 + let diagonals = new Map(); + + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + // 横纵坐标之差可以作为一条对角线的 ID + let diagonalID = i - j; + if (!diagonals.has(diagonalID)) { + diagonals.set(diagonalID, []); + } + diagonals.get(diagonalID).push(mat[i][j]); + } + } + + // 从数组末尾删除元素效率较高,所以我们把数组倒序排序 + for (let diagonal of diagonals.values()) { + diagonal.sort((a, b) => b - a); + } + + // 把排序结果回填二维矩阵 + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + let diagonal = diagonals.get(i - j); + mat[i][j] = diagonal.pop(); + } + } + + return mat; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def diagonalSort(self, mat: List[List[int]]) -> List[List[int]]: + m, n = len(mat), len(mat[0]) + + # 存储所有对角线的元素列表 + diagonals = {} + + for i in range(m): + for j in range(n): + # 横纵坐标之差可以作为一条对角线的 ID + diagonalID = i - j + if diagonalID not in diagonals: + diagonals[diagonalID] = [] + diagonals[diagonalID].append(mat[i][j]) + + # 从数组末尾删除元素效率较高,所以我们把列表倒序排序 + for diagonal in diagonals.values(): + diagonal.sort(reverse=True) + + # 把排序结果回填矩阵 + for i in range(m): + for j in range(n): + diagonal = diagonals[i - j] + mat[i][j] = diagonal.pop() + + return mat +``` + +https://leetcode.cn/problems/sort-the-matrix-diagonally 的多语言解法👆 + +https://leetcode.cn/problems/squares-of-a-sorted-array 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector sortedSquares(vector& nums) { + int n = nums.size(); + // 两个指针分别初始化在正负子数组绝对值最大的元素索引 + int i = 0, j = n - 1; + // 得到的有序结果是降序的 + int p = n - 1; + vector res(n); + // 执行双指针合并有序数组的逻辑 + while (i <= j) { + if (abs(nums[i]) > abs(nums[j])) { + res[p] = nums[i] * nums[i]; + i++; + } else { + res[p] = nums[j] * nums[j]; + j--; + } + p--; + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func sortedSquares(nums []int) []int { + n := len(nums) + // 两个指针分别初始化在正负子数组绝对值最大的元素索引 + i, j := 0, n-1 + // 得到的有序结果是降序的 + p := n - 1 + res := make([]int, n) + // 执行双指针合并有序数组的逻辑 + for i <= j { + if math.Abs(float64(nums[i])) > math.Abs(float64(nums[j])) { + res[p] = nums[i] * nums[i] + i++ + } else { + res[p] = nums[j] * nums[j] + j-- + } + p-- + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] sortedSquares(int[] nums) { + int n = nums.length; + // 两个指针分别初始化在正负子数组绝对值最大的元素索引 + int i = 0, j = n - 1; + // 得到的有序结果是降序的 + int p = n - 1; + int[] res = new int[n]; + // 执行双指针合并有序数组的逻辑 + while (i <= j) { + if (Math.abs(nums[i]) > Math.abs(nums[j])) { + res[p] = nums[i] * nums[i]; + i++; + } else { + res[p] = nums[j] * nums[j]; + j--; + } + p--; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @return {number[]} + */ +var sortedSquares = function(nums) { + let n = nums.length; + // 两个指针分别初始化在正负子数组绝对值最大的元素索引 + let i = 0, j = n - 1; + // 得到的有序结果是降序的 + let p = n - 1; + let res = new Array(n); + // 执行双指针合并有序数组的逻辑 + while (i <= j) { + if (Math.abs(nums[i]) > Math.abs(nums[j])) { + res[p] = nums[i] * nums[i]; + i++; + } else { + res[p] = nums[j] * nums[j]; + j--; + } + p--; + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + n = len(nums) + # 两个指针分别初始化在正负子数组绝对值最大的元素索引 + i, j = 0, n - 1 + # 得到的有序结果是降序的 + p = n - 1 + res = [0] * n + # 执行双指针合并有序数组的逻辑 + while i <= j: + if abs(nums[i]) > abs(nums[j]): + res[p] = nums[i] * nums[i] + i += 1 + else: + res[p] = nums[j] * nums[j] + j -= 1 + p -= 1 + return res +``` + +https://leetcode.cn/problems/squares-of-a-sorted-array 的多语言解法👆 + +https://leetcode.cn/problems/stone-game 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool stoneGame(vector& piles) { + return true; + } +}; +``` + +```go +// by chatGPT (go) +func stoneGame(piles []int) bool { + return true +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean stoneGame(int[] piles) { + return true; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} piles + * @return {boolean} + */ +var stoneGame = function(piles) { + return true; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def stoneGame(self, piles: List[int]) -> bool: + return True +``` + +https://leetcode.cn/problems/stone-game 的多语言解法👆 + +https://leetcode.cn/problems/string-to-integer-atoi 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int myAtoi(string str) { + int n = str.length(); + int i = 0; + // 记录正负号 + int sign = 1; + // 用 long 避免 int 溢出 + long res = 0; + // 跳过前导空格 + while (i < n && str[i] == ' ') { + i++; + } + if (i == n) { + return 0; + } + + // 记录符号位 + if (str[i] == '-') { + sign = -1; + i++; + } else if (str[i] == '+') { + i++; + } + if (i == n) { + return 0; + } + + // 统计数字位 + while (i < n && '0' <= str[i] && str[i] <= '9') { + res = res * 10 + str[i] - '0'; + if (res > INT_MAX) { + break; + } + i++; + } + // 如果溢出,强转成 int 就会和真实值不同 + if ((int) res != res) { + return sign == 1 ? INT_MAX : INT_MIN; + } + return (int) res * sign; + } +}; +``` + +```go +// by chatGPT (go) +import "math" + +func myAtoi(str string) int { + n := len(str) + i := 0 + // 记录正负号 + sign := 1 + // 用 long 避免 int 溢出 + var res int64 = 0 + // 跳过前导空格 + for i < n && str[i] == ' ' { + i++ + } + if i == n { + return 0 + } + // 记录符号位 + if str[i] == '-' { + sign = -1 + i++ + } else if str[i] == '+' { + i++ + } + if i == n { + return 0 + } + // 统计数字位 + for i < n && '0' <= str[i] && str[i] <= '9' { + res = res * 10 + int64(str[i]-'0') + if res > math.MaxInt32 { + break + } + i++ + } + // 如果溢出,强转成 int 就会和真实值不同 + if res > math.MaxInt32 { + if sign == 1 { + return math.MaxInt32 + } else { + return math.MinInt32 + } + } + return int(res) * sign +} +``` + +```java +// by labuladong (java) +class Solution { + public int myAtoi(String str) { + int n = str.length(); + int i = 0; + // 记录正负号 + int sign = 1; + // 用 long 避免 int 溢出 + long res = 0; + // 跳过前导空格 + while (i < n && str.charAt(i) == ' ') { + i++; + } + if (i == n) { + return 0; + } + + // 记录符号位 + if (str.charAt(i) == '-') { + sign = -1; + i++; + } else if (str.charAt(i) == '+') { + i++; + } + if (i == n) { + return 0; + } + + // 统计数字位 + while (i < n && '0' <= str.charAt(i) && str.charAt(i) <= '9') { + res = res * 10 + str.charAt(i) - '0'; + if (res > Integer.MAX_VALUE) { + break; + } + i++; + } + // 如果溢出,强转成 int 就会和真实值不同 + if ((int) res != res) { + return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + return (int) res * sign; + } +} +``` + +```javascript +// by chatGPT (javascript) +var myAtoi = function(str) { + var n = str.length; + var i = 0; + // 记录正负号 + var sign = 1; + // 用 long 避免 int 溢出 + var res = 0; + // 跳过前导空格 + while (i < n && str.charAt(i) == ' ') { + i++; + } + if (i === n) { + return 0; + } + + // 记录符号位 + if (str.charAt(i) == '-') { + sign = -1; + i++; + } else if (str.charAt(i) == '+') { + i++; + } + if (i === n) { + return 0; + } + + // 统计数字位 + while (i < n && '0' <= str.charAt(i) && str.charAt(i) <= '9') { + res = res * 10 + str.charAt(i) - '0'; + if (res > 2147483647) { + break; + } + i++; + } + // 如果溢出,强转成 int 就会和真实值不同 + if (Math.trunc(res) !== res) { + return sign === 1 ? 2147483647 : -2147483648; + } + return res * sign; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def myAtoi(self, str: str) -> int: + n = len(str) + i = 0 + # 记录正负号 + sign = 1 + # 用 long 避免 int 溢出 + res = 0 + # 跳过前导空格 + while i < n and str[i] == ' ': + i += 1 + if i == n: + return 0 + + # 记录符号位 + if str[i] == '-': + sign = -1 + i += 1 + elif str[i] == '+': + i += 1 + if i == n: + return 0 + + # 统计数字位 + while i < n and '0' <= str[i] <= '9': + res = res * 10 + ord(str[i]) - ord('0') + if res > 2 ** 31 - 1: + break + i += 1 + # 如果溢出,强转成 int 就会和真实值不同 + if res != int(res): + return sign * (2 ** 31 - 1) if sign == 1 else -2 ** 31 + return int(res) * sign +``` + +https://leetcode.cn/problems/string-to-integer-atoi 的多语言解法👆 + +https://leetcode.cn/problems/subsets 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + vector> res; + vector> subsets(vector& nums) { + // 记录走过的路径 + vector track; + backtrack(nums, 0, track); + return res; + } + + void backtrack(vector& nums, int start, vector& track) { + res.push_back(track); + for (int i = start; i < nums.size(); i++) { + // 做选择 + track.push_back(nums[i]); + // 回溯 + backtrack(nums, i + 1, track); + // 撤销选择 + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +// +// subsets is a function that returns all possible subsets of an array of integers. +func subsets(nums []int) [][]int { + res := [][]int{} + track := []int{} + backtrack(nums, 0, track, &res) + return res +} + +func backtrack(nums []int, start int, track []int, res *[][]int) { + temp := make([]int, len(track)) + copy(temp, track) + *res = append(*res, temp) + for i := start; i < len(nums); i++ { + // 做选择 + track = append(track, nums[i]) + // 回溯 + backtrack(nums, i+1, track, res) + // 撤销选择 + track = track[:len(track)-1] + } +} +``` + +```java +// by chatGPT (java) +class Solution { + //定义二维数组res用于存储结果 + List> res = new LinkedList<>(); + + public List> subsets(int[] nums) { + //定义路径数组 + List track = new LinkedList<>(); + backtrack(nums, 0, track); + + return res; + } + + public void backtrack(int[] nums, int start, List track) { + //添加路径数组到结果数组中 + res.add(new LinkedList<>(track)); + //for循环遍历数组nums + for (int i = start; i < nums.length; i++) { + //做选择,将选择添加到路径数组中 + track.add(nums[i]); + //回溯,继续向后遍历 + backtrack(nums, i + 1, track); + //撤销选择,将选择从路径中删除 + track.remove(track.size() - 1); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var subsets = function(nums) { + var res = []; + var track = []; + // 记录走过的路径 + backtrack(nums, 0, track); + return res; + + function backtrack(nums, start, track) { + res.push([...track]); + for (var i = start; i < nums.length; i++) { + // 做选择 + track.push(nums[i]); + // 回溯 + backtrack(nums, i + 1, track); + // 撤销选择 + track.pop(); + } + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def subsets(self, nums: List[int]) -> List[List[int]]: + res = [] + track = [] + # 记录走过的路径 + self.backtrack(nums, 0, track, res) + return res + + def backtrack(self, nums, start, track, res): + res.append(track[:]) + for i in range(start, len(nums)): + # 做选择 + track.append(nums[i]) + # 回溯 + self.backtrack(nums, i + 1, track, res) + # 撤销选择 + track.pop() +``` + +https://leetcode.cn/problems/subsets 的多语言解法👆 + +https://leetcode.cn/problems/sudoku-solver 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + void solveSudoku(vector>& board) { + backtrack(board, 0, 0); + } + + bool backtrack(vector>& board, int i, int j) { + int m = 9, n = 9; + if (j == n) { + // 穷举到最后一列的话就换到下一行重新开始。 + return backtrack(board, i + 1, 0); + } + if (i == m) { + // 找到一个可行解,触发 base case + return true; + } + + if (board[i][j] != '.') { + // 如果有预设数字,不用我们穷举 + return backtrack(board, i, j + 1); + } + + for (char ch = '1'; ch <= '9'; ch++) { + // 如果遇到不合法的数字,就跳过 + if (!isValid(board, i, j, ch)) + continue; + + board[i][j] = ch; + // 如果找到一个可行解,立即结束 + if (backtrack(board, i, j + 1)) { + return true; + } + board[i][j] = '.'; + } + // 穷举完 1~9,依然没有找到可行解,此路不通 + return false; + } + + bool isValid(vector>& board, int r, int c, char n) { + for (int i = 0; i < 9; i++) { + // 判断行是否存在重复 + if (board[r][i] == n) return false; + // 判断列是否存在重复 + if (board[i][c] == n) return false; + // 判断 3 x 3 方框是否存在重复 + if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n) + return false; + } + return true; + } +}; +``` + +```go +// by chatGPT (go) +func solveSudoku(board [][]byte) { + backtrack(board, 0, 0) +} + +func backtrack(board [][]byte, i int, j int) bool { + m, n := 9, 9 + if j == n { + // 穷举到最后一列的话就换到下一行重新开始。 + return backtrack(board, i+1, 0) + } + if i == m { + // 找到一个可行解,触发 base case + return true + } + + if board[i][j] != '.' { + // 如果有预设数字,不用我们穷举 + return backtrack(board, i, j+1) + } + + for ch := '1'; ch <= '9'; ch++ { + // 如果遇到不合法的数字,就跳过 + if !isValid(board, i, j, byte(ch)) { + continue + } + + board[i][j] = byte(ch) + // 如果找到一个可行解,立即结束 + if backtrack(board, i, j+1) { + return true + } + board[i][j] = '.' + } + // 穷举完 1~9,依然没有找到可行解,此路不通 + return false +} + +// 判断 board[i][j] 是否可以填入 n +func isValid(board [][]byte, r int, c int, n byte) bool { + for i := 0; i < 9; i++ { + // 判断行是否存在重复 + if board[r][i] == n { + return false + } + // 判断列是否存在重复 + if board[i][c] == n { + return false + } + // 判断 3 x 3 方框是否存在重复 + if board[(r/3)*3+i/3][(c/3)*3+i%3] == n { + return false + } + } + return true +} +``` + +```java +// by labuladong (java) +class Solution { + public void solveSudoku(char[][] board) { + backtrack(board, 0, 0); + } + + boolean backtrack(char[][] board, int i, int j) { + int m = 9, n = 9; + if (j == n) { + // 穷举到最后一列的话就换到下一行重新开始。 + return backtrack(board, i + 1, 0); + } + if (i == m) { + // 找到一个可行解,触发 base case + return true; + } + + if (board[i][j] != '.') { + // 如果有预设数字,不用我们穷举 + return backtrack(board, i, j + 1); + } + + for (char ch = '1'; ch <= '9'; ch++) { + // 如果遇到不合法的数字,就跳过 + if (!isValid(board, i, j, ch)) + continue; + + board[i][j] = ch; + // 如果找到一个可行解,立即结束 + if (backtrack(board, i, j + 1)) { + return true; + } + board[i][j] = '.'; + } + // 穷举完 1~9,依然没有找到可行解,此路不通 + return false; + } + + // 判断 board[i][j] 是否可以填入 n + boolean isValid(char[][] board, int r, int c, char n) { + for (int i = 0; i < 9; i++) { + // 判断行是否存在重复 + if (board[r][i] == n) return false; + // 判断列是否存在重复 + if (board[i][c] == n) return false; + // 判断 3 x 3 方框是否存在重复 + if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n) + return false; + } + return true; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {character[][]} board + * @return {void} Do not return anything, modify board in-place instead. + */ +var solveSudoku = function(board) { + backtrack(board, 0, 0); +}; + +var backtrack = function(board, i, j) { + const m = 9, n = 9; + if (j == n) { + // 穷举到最后一列的话就换到下一行重新开始。 + return backtrack(board, i + 1, 0); + } + if (i == m) { + // 找到一个可行解,触发 base case + return true; + } + + if (board[i][j] != '.') { + // 如果有预设数字,不用我们穷举 + return backtrack(board, i, j + 1); + } + + for (let ch = '1'; ch <= '9'; ch++) { + // 如果遇到不合法的数字,就跳过 + if (!isValid(board, i, j, ch)) + continue; + + board[i][j] = ch; + // 如果找到一个可行解,立即结束 + if (backtrack(board, i, j + 1)) { + return true; + } + board[i][j] = '.'; + } + // 穷举完 1~9,依然没有找到可行解,此路不通 + return false; +} + +var isValid = function(board, r, c, n) { + for (let i = 0; i < 9; i++) { + // 判断行是否存在重复 + if (board[r][i] == n) return false; + // 判断列是否存在重复 + if (board[i][c] == n) return false; + // 判断 3 x 3 方框是否存在重复 + if (board[Math.floor(r/3)*3 + Math.floor(i/3)][Math.floor(c/3)*3 + i%3] == n) + return false; + } + return true; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def solveSudoku(self, board: List[List[str]]) -> None: + """ + Do not return anything, modify board in-place instead. + """ + self.backtrack(board, 0, 0) + + def backtrack(self, board: List[List[str]], i: int, j: int) -> bool: + m, n = 9, 9 + if j == n: + # 穷举到最后一列的话就换到下一行重新开始。 + return self.backtrack(board, i + 1, 0) + if i == m: + # 找到一个可行解,触发 base case + return True + + if board[i][j] != '.': + # 如果有预设数字,不用我们穷举 + return self.backtrack(board, i, j + 1) + + for ch in range(1, 10): + ch = str(ch) + # 如果遇到不合法的数字,就跳过 + if not self.isValid(board, i, j, ch): + continue + + board[i][j] = ch + # 如果找到一个可行解,立即结束 + if self.backtrack(board, i, j + 1): + return True + board[i][j] = '.' + # 穷举完 1~9,依然没有找到可行解,此路不通 + return False + + # 判断 board[i][j] 是否可以填入 n + def isValid(self, board: List[List[str]], r: int, c: int, n: str) -> bool: + for i in range(9): + # 判断行是否存在重复 + if board[r][i] == n: + return False + # 判断列是否存在重复 + if board[i][c] == n: + return False + # 判断 3 x 3 方框是否存在重复 + if board[(r // 3) * 3 + i // 3][(c // 3) * 3 + i % 3] == n: + return False + return True +``` + +https://leetcode.cn/problems/sudoku-solver 的多语言解法👆 + +https://leetcode.cn/problems/super-egg-drop 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int superEggDrop(int K, int N) { + // m 最多不会超过 N 次(线性扫描) + vector> dp(K + 1, vector(N + 1)); + // base case: + // dp[0][..] = 0 + // dp[..][0] = 0 + // C++默认初始化为0 + int m = 0; + while (dp[K][m] < N) { + m++; + for (int k = 1; k <= K; k++) + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; + } + return m; + } +}; +``` + +```go +// by chatGPT (go) +func superEggDrop(K int, N int) int { + // m 最多不会超过 N 次(线性扫描) + dp := make([][]int, K + 1) + for i := 0; i <= K; i++ { + dp[i] = make([]int, N + 1) + } + // base case: + // dp[0][..] = 0 + // dp[..][0] = 0 + // Go 默认初始化数组都为 0 + m := 0 + for dp[K][m] < N { + m++ + for k := 1; k <= K; k++ { + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1 + } + } + return m +} +``` + +```java +// by labuladong (java) +class Solution { + public int superEggDrop(int K, int N) { + // m 最多不会超过 N 次(线性扫描) + int[][] dp = new int[K + 1][N + 1]; + // base case: + // dp[0][..] = 0 + // dp[..][0] = 0 + // Java 默认初始化数组都为 0 + int m = 0; + while (dp[K][m] < N) { + m++; + for (int k = 1; k <= K; k++) + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; + } + return m; + } +} +``` + +```javascript +// by chatGPT (javascript) +var superEggDrop = function(K, N) { + // m 最多不会超过 N 次(线性扫描) + var dp = new Array(K + 1).fill(0).map(x => new Array(N + 1).fill(0)); + // base case: + // dp[0][..] = 0 + // dp[..][0] = 0 + // JavaScript 默认初始化数组都为 undefined + + var m = 0; + while (dp[K][m] < N) { + m++; + for (var k = 1; k <= K; k++) + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; + } + return m; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def superEggDrop(self, K: int, N: int) -> int: + # m 最多不会超过 N 次(线性扫描) + dp = [[0] * (N + 1) for _ in range(K + 1)] + # base case: + # dp[0][..] = 0 + # dp[..][0] = 0 + # Java 默认初始化数组都为 0 + m = 0 + while dp[K][m] < N: + m += 1 + for k in range(1, K + 1): + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1 + return m +``` + +https://leetcode.cn/problems/super-egg-drop 的多语言解法👆 + +https://leetcode.cn/problems/super-pow 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + + int base = 1337; + + // 计算 a 的 k 次方然后与 base 求模的结果 + int mypow(int a, int k) { + // 对因子求模 + a %= base; + int res = 1; + for (int _ = 0; _ < k; _++) { + // 这里有乘法,是潜在的溢出点 + res *= a; + // 对乘法结果求模 + res %= base; + } + return res; + } + + int superPow(int a, vector& b) { + if (b.empty()) return 1; + int last = b.back(); + b.pop_back(); + + int part1 = mypow(a, last); + int part2 = mypow(superPow(a, b), 10); + // 每次乘法都要求模 + return (part1 * part2) % base; + } +}; +``` + +```go +// by chatGPT (go) +const base = 1337 + +// 计算 a 的 k 次方然后与 base 求模的结果 +func mypow(a, k int) int { + // 对因子求模 + a %= base + res := 1 + for i := 0; i < k; i++ { + // 这里有乘法,是潜在的溢出点 + res *= a + // 对乘法结果求模 + res %= base + } + return res +} + +func superPow(a int, b []int) int { + if len(b) == 0 { + return 1 + } + last := b[len(b)-1] + b = b[:len(b)-1] + + part1 := mypow(a, last) + part2 := mypow(superPow(a, b), 10) + // 每次乘法都要求模 + return (part1 * part2) % base +} +``` + +```java +// by chatGPT (java) +class Solution { + int base = 1337; + + // 计算 a 的 k 次方然后与 base 求模的结果 + private int mypow(int a, int k) { + // 对因子求模 + a %= base; + int res = 1; + for (int i = 0; i < k; i++) { + // 这里有乘法,是潜在的溢出点 + res *= a; + // 对乘法结果求模 + res %= base; + } + return res; + } + + public int superPow(int a, int[] b) { + if (b.length == 0) return 1; + int last = b[b.length - 1]; + int[] newB = Arrays.copyOf(b, b.length - 1); + + int part1 = mypow(a, last); + int part2 = mypow(superPow(a, newB), 10); + // 每次乘法都要求模 + return (part1 * part2) % base; + } +} +``` + +```javascript +// by chatGPT (javascript) +var superPow = function(a, b) { + const base = 1337; + + function mypow(a, k) { + // 对因子求模 + a %= base; + let res = 1; + for (let _ = 0; _ < k; _++) { + // 这里有乘法,是潜在的溢出点 + res *= a; + // 对乘法结果求模 + res %= base; + } + return res; + } + + if (b.length === 0) return 1; + const last = b.pop(); + + const part1 = mypow(a, last); + const part2 = mypow(superPow(a, b), 10); + // 每次乘法都要求模 + return (part1 * part2) % base; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.base = 1337 + + # 计算 a 的 k 次方然后与 base 求模的结果 + def mypow(self, a: int, k: int) -> int: + # 对因子求模 + a %= self.base + res = 1 + for _ in range(k): + # 这里有乘法,是潜在的溢出点 + res = (res * a) % self.base + return res + + def superPow(self, a: int, b: List[int]) -> int: + if not b: + return 1 + last = b.pop() + + part1 = self.mypow(a, last) + part2 = self.mypow(self.superPow(a, b), 10) + # 每次乘法都要求模 + return (part1 * part2) % self.base +``` + +https://leetcode.cn/problems/super-pow 的多语言解法👆 + +https://leetcode.cn/problems/super-ugly-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int nthSuperUglyNumber(int n, vector& primes) { + // 优先队列中装三元组 int[] {product, prime, pi} + // 其中 product 代表链表节点的值,prime 是计算下一个节点所需的质数因子,pi 代表链表上的指针 + priority_queue, vector>, greater>> pq; + + // 把多条链表的头结点加入优先级队列 + for (int i = 0; i < primes.size(); i++) { + pq.push({ 1, primes[i], 1 }); + } + + // 可以理解为最终合并的有序链表(结果链表) + vector ugly(n + 1); + // 可以理解为结果链表上的指针 + int p = 1; + + while (p <= n) { + // 取三个链表的最小结点 + vector pair = pq.top(); + pq.pop(); + int product = pair[0]; + int prime = pair[1]; + int index = pair[2]; + + // 避免结果链表出现重复元素 + if (product != ugly[p - 1]) { + // 接到结果链表上 + ugly[p] = product; + p++; + } + + // 生成下一个节点加入优先级队列 + vector nextPair = { ugly[index] * prime, prime, index + 1 }; + pq.push(nextPair); + } + return ugly[n]; + } +}; +``` + +```go +// by chatGPT (go) +func nthSuperUglyNumber(n int, primes []int) int { + // 优先队列中装三元组 int[] {product, prime, pi} + // 其中 product 代表链表节点的值,prime 是计算下一个节点所需的质数因子,pi 代表链表上的指针 + pq := make(PriorityQueue, 0) + heap.Init(&pq) + + // 把多条链表的头结点加入优先级队列 + for i := 0; i < len(primes); i++ { + pq.Push(&IntTriples{product: 1, prime: primes[i], pi: 1}) + } + + // 可以理解为最终合并的有序链表(结果链表) + ugly := make([]int, n+1) + // 可以理解为结果链表上的指针 + p := 1 + + for p <= n { + // 取三个链表的最小结点 + pair := heap.Pop(&pq).(*IntTriples) + product, prime, index := pair.product, pair.prime, pair.pi + + // 避免结果链表出现重复元素 + if product != ugly[p-1] { + // 接到结果链表上 + ugly[p] = product + p++ + } + + // 生成下一个节点加入优先级队列 + nextPair := &IntTriples{product: ugly[index] * prime, prime: prime, pi: index + 1} + heap.Push(&pq, nextPair) + } + + return ugly[n] +} + +// 优先队列的封装 +type PriorityQueue []*IntTriples + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].product < pq[j].product +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(*IntTriples) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} + +// 封装三元组 +type IntTriples struct { + product int + prime int + pi int +} +``` + +```java +// by labuladong (java) +class Solution { + public int nthSuperUglyNumber(int n, int[] primes) { + // 优先队列中装三元组 int[] {product, prime, pi} + // 其中 product 代表链表节点的值,prime 是计算下一个节点所需的质数因子,pi 代表链表上的指针 + PriorityQueue pq = new PriorityQueue<>((a, b) -> { + return a[0] - b[0]; + }); + + // 把多条链表的头结点加入优先级队列 + for (int i = 0; i < primes.length; i++) { + pq.offer(new int[]{ 1, primes[i], 1 }); + } + + // 可以理解为最终合并的有序链表(结果链表) + int[] ugly = new int[n + 1]; + // 可以理解为结果链表上的指针 + int p = 1; + + while (p <= n) { + // 取三个链表的最小结点 + int[] pair = pq.poll(); + int product = pair[0]; + int prime = pair[1]; + int index = pair[2]; + + // 避免结果链表出现重复元素 + if (product != ugly[p - 1]) { + // 接到结果链表上 + ugly[p] = product; + p++; + } + + // 生成下一个节点加入优先级队列 + int[] nextPair = new int[]{ugly[index] * prime, prime, index + 1}; + pq.offer(nextPair); + } + return ugly[n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var nthSuperUglyNumber = function(n, primes) { + // 优先队列中装三元组 int[] {product, prime, pi} + // 其中 product 代表链表节点的值,prime 是计算下一个节点所需的质数因子,pi 代表链表上的指针 + let pq = new PriorityQueue((a, b) => { + return a[0] - b[0]; + }); + + // 把多条链表的头结点加入优先级队列 + for (let i = 0; i < primes.length; i++) { + pq.offer([ 1, primes[i], 1 ]); + } + + // 可以理解为最终合并的有序链表(结果链表) + let ugly = new Array(n + 1); + // 可以理解为结果链表上的指针 + let p = 1; + + while (p <= n) { + // 取三个链表的最小结点 + let pair = pq.poll(); + let product = pair[0]; + let prime = pair[1]; + let index = pair[2]; + + // 避免结果链表出现重复元素 + if (product != ugly[p - 1]) { + // 接到结果链表上 + ugly[p] = product; + p++; + } + + // 生成下一个节点加入优先级队列 + let nextPair = [ugly[index] * prime, prime, index + 1]; + pq.offer(nextPair); + } + return ugly[n]; +}; +``` + +```python +# by chatGPT (python) +import heapq + +class Solution: + def nthSuperUglyNumber(self, n: int, primes: List[int]) -> int: + # 优先队列中装三元组 int[] {product, prime, pi} + # 其中 product 代表链表节点的值,prime 是计算下一个节点所需的质数因子,pi 代表链表上的指针 + pq = [] + + # 把多条链表的头结点加入优先级队列 + for i in range(len(primes)): + heapq.heappush(pq, [1, primes[i], 1]) + + # 可以理解为最终合并的有序链表(结果链表) + ugly = [0] * (n+1) + # 可以理解为结果链表上的指针 + p = 1 + + while p <= n: + # 取三个链表的最小结点 + product, prime, index = heapq.heappop(pq) + + # 避免结果链表出现重复元素 + if product != ugly[p - 1]: + # 接到结果链表上 + ugly[p] = product + p += 1 + + # 生成下一个节点加入优先级队列 + nextPair = [ugly[index] * prime, prime, index + 1] + heapq.heappush(pq, nextPair) + + return ugly[n] +``` + +https://leetcode.cn/problems/super-ugly-number 的多语言解法👆 + +https://leetcode.cn/problems/surrounded-regions 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + void solve(vector>& board) { + if (board.empty()) return; + + int m = board.size(); + int n = board[0].size(); + // 给 dummy 留一个额外位置 + UF uf = UF(m * n + 1); + int dummy = m * n; + // 将首列和末列的 O 与 dummy 连通 + for (int i = 0; i < m; i++) { + if (board[i][0] == 'O') + uf.union(i * n, dummy); + if (board[i][n - 1] == 'O') + uf.union(i * n + n - 1, dummy); + } + // 将首行和末行的 O 与 dummy 连通 + for (int j = 0; j < n; j++) { + /** + ![](../pictures/unionfind应用/3.jpg) + */ + if (board[0][j] == 'O') + uf.union(j, dummy); + if (board[m - 1][j] == 'O') + uf.union(n * (m - 1) + j, dummy); + } + // 方向数组 d 是上下左右搜索的常用手法 + vector> d = {{1, 0}, {0, 1}, {0, -1}, {-1, 0}}; + for (int i = 1; i < m - 1; i++) + for (int j = 1; j < n - 1; j++) + if (board[i][j] == 'O') + // 将此 O 与上下左右的 O 连通 + for (int k = 0; k < 4; k++) { + int x = i + d[k][0]; + int y = j + d[k][1]; + if (board[x][y] == 'O') + uf.union(x * n + y, i * n + j); + } + // 所有不和 dummy 连通的 O,都要被替换 + for (int i = 1; i < m - 1; i++) + for (int j = 1; j < n - 1; j++) + if (!uf.connected(dummy, i * n + j)) + board[i][j] = 'X'; + } +}; + +class UF { +public: + // 记录连通分量个数 + int count; + // 存储若干棵树 + vector parent; + // 记录树的“重量” + vector size; + + UF(int n) { + this->count = n; + parent = vector(n); + size = vector(n); + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + /* 将 p 和 q 连通 */ + void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + count--; + } + + /* 判断 p 和 q 是否互相连通 */ + bool connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } +}; +``` + +```go +// by chatGPT (go) +func solve(board [][]byte) { + if len(board) == 0 { + return + } + + m := len(board) + n := len(board[0]) + // 给 dummy 留一个额外位置 + uf := NewUF(m*n + 1) + dummy := m*n + // 将首列和末列的 O 与 dummy 连通 + for i := 0; i < m; i++ { + if board[i][0] == 'O' { + uf.union(i*n, dummy) + } + if board[i][n-1] == 'O' { + uf.union(i*n+n-1, dummy) + } + } + // 将首行和末行的 O 与 dummy 连通 + for j := 0; j < n; j++ { + if board[0][j] == 'O' { + uf.union(j, dummy) + } + if board[m-1][j] == 'O' { + uf.union(n*(m-1)+j, dummy) + } + } + // 方向数组 d 是上下左右搜索的常用手法 + d := [][]int{{1, 0}, {0, 1}, {0, -1}, {-1, 0}} + for i := 1; i < m-1; i++ { + for j := 1; j < n-1; j++ { + if board[i][j] == 'O' { + // 将此 O 与上下左右的 O 连通 + for k := 0; k < 4; k++ { + x := i + d[k][0] + y := j + d[k][1] + if board[x][y] == 'O' { + uf.union(x*n+y, i*n+j) + } + } + } + } + } + // 所有不和 dummy 连通的 O,都要被替换 + for i := 1; i < m-1; i++ { + for j := 1; j < n-1; j++ { + if !uf.connected(dummy, i*n+j) { + board[i][j] = 'X' + } + } + } +} + +type UF struct { + // 记录连通分量个数 + count int + // 存储若干棵树 + parent []int + // 记录树的“重量” + size []int +} + +func NewUF(n int) *UF { + u := &UF{count: n, parent: make([]int, n), size: make([]int, n)} + for i := 0; i < n; i++ { + u.parent[i] = i + u.size[i] = 1 + } + return u +} + +/* 将 p 和 q 连通 */ +func (u *UF) union(p, q int) { + rootP := u.find(p) + rootQ := u.find(q) + if rootP == rootQ { + return + } + + // 小树接到大树下面,较平衡 + if u.size[rootP] > u.size[rootQ] { + u.parent[rootQ] = rootP + u.size[rootP] += u.size[rootQ] + } else { + u.parent[rootP] = rootQ + u.size[rootQ] += u.size[rootP] + } + u.count-- +} + +/* 判断 p 和 q 是否互相连通 */ +func (u *UF) connected(p, q int) bool { + return u.find(p) == u.find(q) +} + +/* 返回节点 x 的根节点 */ +func (u *UF) find(x int) int { + for u.parent[x] != x { + // 进行路径压缩 + u.parent[x] = u.parent[u.parent[x]] + x = u.parent[x] + } + return x +} + +func (u *UF) Count() int { + return u.count +} +``` + +```java +// by labuladong (java) +class Solution { + public void solve(char[][] board) { + if (board.length == 0) return; + + int m = board.length; + int n = board[0].length; + // 给 dummy 留一个额外位置 + UF uf = new UF(m * n + 1); + int dummy = m * n; + // 将首列和末列的 O 与 dummy 连通 + for (int i = 0; i < m; i++) { + if (board[i][0] == 'O') + uf.union(i * n, dummy); + if (board[i][n - 1] == 'O') + uf.union(i * n + n - 1, dummy); + } + // 将首行和末行的 O 与 dummy 连通 + for (int j = 0; j < n; j++) { + /** + ![](../pictures/unionfind应用/3.jpg) + */ + if (board[0][j] == 'O') + uf.union(j, dummy); + if (board[m - 1][j] == 'O') + uf.union(n * (m - 1) + j, dummy); + } + // 方向数组 d 是上下左右搜索的常用手法 + int[][] d = new int[][]{{1, 0}, {0, 1}, {0, -1}, {-1, 0}}; + for (int i = 1; i < m - 1; i++) + for (int j = 1; j < n - 1; j++) + if (board[i][j] == 'O') + // 将此 O 与上下左右的 O 连通 + for (int k = 0; k < 4; k++) { + int x = i + d[k][0]; + int y = j + d[k][1]; + if (board[x][y] == 'O') + uf.union(x * n + y, i * n + j); + } + // 所有不和 dummy 连通的 O,都要被替换 + for (int i = 1; i < m - 1; i++) + for (int j = 1; j < n - 1; j++) + if (!uf.connected(dummy, i * n + j)) + board[i][j] = 'X'; + } +} + +class UF { + // 记录连通分量个数 + private int count; + // 存储若干棵树 + private int[] parent; + // 记录树的“重量” + private int[] size; + + public UF(int n) { + this.count = n; + parent = new int[n]; + size = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + /* 将 p 和 q 连通 */ + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + count--; + } + + /* 判断 p 和 q 是否互相连通 */ + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + private int find(int x) { + while (parent[x] != x) { + // 进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + public int count() { + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {character[][]} board + * @return {void} Do not return anything, modify board in-place instead. + */ +var solve = function(board) { + if (board.length == 0) return; + + const m = board.length; + const n = board[0].length; + // 给 dummy 留一个额外位置 + const uf = new UF(m * n + 1); + const dummy = m * n; + // 将首列和末列的 O 与 dummy 连通 + for (let i = 0; i < m; i++) { + if (board[i][0] == 'O') + uf.union(i * n, dummy); + if (board[i][n - 1] == 'O') + uf.union(i * n + n - 1, dummy); + } + // 将首行和末行的 O 与 dummy 连通 + + const d = [[1, 0], [0, 1], [0, -1], [-1, 0]]; + for (let j = 0; j < n; j++) { + /** + ![](../pictures/unionfind应用/3.jpg) + */ + if (board[0][j] == 'O') + uf.union(j, dummy); + if (board[m - 1][j] == 'O') + uf.union(n * (m - 1) + j, dummy); + } + // 方向数组 d 是上下左右搜索的常用手法 + + for (let i = 1; i < m - 1; i++) + for (let j = 1; j < n - 1; j++) + if (board[i][j] == 'O') + // 将此 O 与上下左右的 O 连通 + for (let k = 0; k < 4; k++) { + const x = i + d[k][0]; + const y = j + d[k][1]; + if (board[x][y] == 'O') + uf.union(x * n + y, i * n + j); + } + // 所有不和 dummy 连通的 O,都要被替换 + for (let i = 1; i < m - 1; i++) + for (let j = 1; j < n - 1; j++) + if (!uf.connected(dummy, i * n + j)) + board[i][j] = 'X'; +} + +class UF { + // 记录连通分量个数 + count; + // 存储若干棵树 + parent; + // 记录树的“重量” + size; + + constructor(n) { + this.count = n; + this.parent = []; + this.size = []; + for (let i = 0; i < n; i++) { + this.parent[i] = i; + this.size[i] = 1; + } + } + + /* 将 p 和 q 连通 */ + union(p, q) { + const rootP = this.find(p); + const rootQ = this.find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (this.size[rootP] > this.size[rootQ]) { + this.parent[rootQ] = rootP; + this.size[rootP] += this.size[rootQ]; + } else { + this.parent[rootP] = rootQ; + this.size[rootQ] += this.size[rootP]; + } + this.count--; + } + + /* 判断 p 和 q 是否互相连通 */ + connected(p, q) { + const rootP = this.find(p); + const rootQ = this.find(q); + // 处于同一棵树上的节点,相互连通 + return rootP == rootQ; + } + + /* 返回节点 x 的根节点 */ + find(x) { + while (this.parent[x] != x) { + // 进行路径压缩 + this.parent[x] = this.parent[this.parent[x]]; + x = this.parent[x]; + } + return x; + } + + count() { + return this.count; + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def solve(self, board: List[List[str]]) -> None: + """ + Do not return anything, modify board in-place instead. + """ + if not board: + return + + m, n = len(board), len(board[0]) + # 给 dummy 留一个额外位置 + uf = UF(m * n + 1) + dummy = m * n + # 将首列和末列的 O 与 dummy 连通 + for i in range(m): + if board[i][0] == 'O': + uf.union(i * n, dummy) + if board[i][n - 1] == 'O': + uf.union(i * n + n - 1, dummy) + # 将首行和末行的 O 与 dummy 连通 + for j in range(n): + # extend up -150 + if board[0][j] == 'O': + uf.union(j, dummy) + if board[m - 1][j] == 'O': + uf.union(n * (m - 1) + j, dummy) + # 方向数组 d 是上下左右搜索的常用手法 + d = [[1, 0], [0, 1], [0, -1], [-1, 0]] + for i in range(1, m - 1): + for j in range(1, n - 1): + if board[i][j] == 'O': + # 将此 O 与上下左右的 O 连通 + for k in range(4): + x = i + d[k][0] + y = j + d[k][1] + if board[x][y] == 'O': + uf.union(x * n + y, i * n + j) + # 所有不和 dummy 连通的 O,都要被替换 + for i in range(1, m - 1): + for j in range(1, n - 1): + if not uf.connected(dummy, i * n + j): + board[i][j] = 'X' + +class UF: + # 记录连通分量个数 + def __init__(self, n: int): + self.count = n + # 存储若干棵树 + self.parent = list(range(n)) + # 记录树的“重量” + self.size = [1] * n + + # 将 p 和 q 连通 + def union(self, p: int, q: int) -> None: + rootP = self.find(p) + rootQ = self.find(q) + if rootP == rootQ: + return + + # 小树接到大树下面,较平衡 + if self.size[rootP] > self.size[rootQ]: + self.parent[rootQ] = rootP + self.size[rootP] += self.size[rootQ] + else: + self.parent[rootP] = rootQ + self.size[rootQ] += self.size[rootP] + self.count -= 1 + + # 判断 p 和 q 是否互相连通 + def connected(self, p: int, q: int) -> bool: + rootP = self.find(p) + rootQ = self.find(q) + # 处于同一棵树上的节点,相互连通 + return rootP == rootQ + + # 返回节点 x 的根节点 + def find(self, x: int) -> int: + while self.parent[x] != x: + # 进行路径压缩 + self.parent[x] = self.parent[self.parent[x]] + x = self.parent[x] + return x + + def count(self) -> int: + return self.count +``` + +https://leetcode.cn/problems/surrounded-regions 的多语言解法👆 + +https://leetcode.cn/problems/swap-nodes-in-pairs 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转, + // 返回翻转后的链表头结点 +public: + ListNode* swapPairs(ListNode* head) { + if (head == nullptr || head->next == nullptr) { //递归结束条件 + return head; + } + ListNode* first = head; + ListNode* second = head->next; + ListNode* others = head->next->next; + // 先把前两个元素翻转 + second->next = first; + // 利用递归定义,将剩下的链表节点两两翻转,接到后面 + first->next = swapPairs(others); + // 现在整个链表都成功翻转了,返回新的头结点 + return second; + /** + ![](../pictures/kgroup/7.jpg) + */ + } +}; +``` + +```go +// by chatGPT (go) +// 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转, +// 返回翻转后的链表头结点 +func swapPairs(head *ListNode) *ListNode { + if head == nil || head.Next == nil { + return head + } + first := head + second := head.Next + others := head.Next.Next + // 先把前两个元素翻转 + second.Next = first + // 利用递归定义,将剩下的链表节点两两翻转,接到后面 + first.Next = swapPairs(others) + // 现在整个链表都成功翻转了,返回新的头结点 + return second +} +``` + +```java +// by labuladong (java) +class Solution { + // 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转, + // 返回翻转后的链表头结点 + public ListNode swapPairs(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode first = head; + ListNode second = head.next; + ListNode others = head.next.next; + // 先把前两个元素翻转 + second.next = first; + // 利用递归定义,将剩下的链表节点两两翻转,接到后面 + first.next = swapPairs(others); + // 现在整个链表都成功翻转了,返回新的头结点 + return second; + /** + ![](../pictures/kgroup/7.jpg) + */ + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转, + * 返回翻转后的链表头结点 + */ +var swapPairs = function(head) { + if (head == null || head.next == null) { + return head; + } + // 先把前两个元素翻转 + let first = head; + let second = head.next; + let others = head.next.next; + second.next = first; + // 利用递归定义,将剩下的链表节点两两翻转,接到后面 + first.next = swapPairs(others); + // 现在整个链表都成功翻转了,返回新的头结点 + return second; + /** + ![](../pictures/kgroup/7.jpg) + */ +}; +``` + +```python +# by chatGPT (python) +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, x): +# self.val = x +# self.next = None + +class Solution: + def swapPairs(self, head: ListNode) -> ListNode: + """ + :type head: ListNode + :rtype: ListNode + 输入以 head 开头的单链表,将这个单链表中的每两个元素翻转, + 返回翻转后的链表头结点 + """ + if not head or not head.next: + # 如果当前节点 head 为空, 或者下一个节点 head.next 为空, 将 head 直接返回 + return head + # 定义三个变量, 分别为当前节点, 当前节点的下一个节点和下下个节点 + first, second, others = head, head.next, head.next.next + # 先把前两个元素翻转 + second.next = first + # 利用递归定义, 将剩下的链表节点两两翻转, 接到后面 + first.next = self.swapPairs(others) + # 现在整个链表都成功翻转了, 返回新的头结点 + return second + """ + 原链表为:head → 1 → 2 → 3 → 4 → null, + 要求链表为:head → 2 → 1 → 4 → 3 → null。 + ![](../pictures/kgroup/7.jpg) + """ +``` + +https://leetcode.cn/problems/swap-nodes-in-pairs 的多语言解法👆 + +https://leetcode.cn/problems/target-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findTargetSumWays(vector& nums, int target) { + if (nums.empty()) return 0; + return dp(nums, 0, target); + } + + // 备忘录 + unordered_map memo; + + int dp(vector& nums, int i, int remain) { + // base case + if (i == nums.size()) { + if (remain == 0) return 1; + return 0; + } + // 把它俩转成字符串才能作为哈希表的键 + string key = to_string(i) + "," + to_string(remain); + // 避免重复计算 + if (memo.count(key)) { + return memo[key]; + } + // 还是穷举 + int result = dp(nums, i + 1, remain - nums[i]) + dp(nums, i + 1, remain + nums[i]); + // 记入备忘录 + memo[key] = result; + return result; + } +}; +``` + +```go +// by chatGPT (go) +func findTargetSumWays(nums []int, target int) int { + // 如果nums为空,则直接返回0 + if len(nums) == 0 { + return 0 + } + // 定义备忘录 + memo := make(map[string]int) + // 调用DP函数 + return dp(nums, 0, target, memo) +} + +func dp(nums []int, i, remain int, memo map[string]int) int { + // base case + if i == len(nums) { + if remain == 0 { + return 1 + } + return 0 + } + // 把它俩转成字符串才能作为哈希表的键 + key := strconv.Itoa(i) + "," + strconv.Itoa(remain) + // 避免重复计算 + if val, ok := memo[key]; ok { + return val + } + // 还是穷举 + result := dp(nums, i+1, remain-nums[i], memo) + dp(nums, i+1, remain+nums[i], memo) + // 记入备忘录 + memo[key] = result + return result +} +``` + +```java +// by labuladong (java) +class Solution { + public int findTargetSumWays(int[] nums, int target) { + if (nums.length == 0) return 0; + return dp(nums, 0, target); + } + + // 备忘录 + HashMap memo = new HashMap<>(); + + int dp(int[] nums, int i, int remain) { + // base case + if (i == nums.length) { + if (remain == 0) return 1; + return 0; + } + // 把它俩转成字符串才能作为哈希表的键 + String key = i + "," + remain; + // 避免重复计算 + if (memo.containsKey(key)) { + return memo.get(key); + } + // 还是穷举 + int result = dp(nums, i + 1, remain - nums[i]) + dp(nums, i + 1, remain + nums[i]); + // 记入备忘录 + memo.put(key, result); + return result; + } +} +``` + +```javascript +// by chatGPT (javascript) +// 传入一个整数数组和目标值target,求有多少种方法使得数组中的元素之和为target,每个元素可以选择加或者减 +var findTargetSumWays = function (nums, target) { + // 数组长度为0直接返回0 + if (nums.length === 0) return 0; + + // 备忘录哈希表,记录已经计算过的子问题的结果,避免重复计算 + const memo = new Map(); + + // 递归函数 + const dp = (nums, i, remain) => { + // 如果已经遍历到了数组的末尾,判断remain是否等于0,如果等于0则返回1,否则返回0 + if (i === nums.length) { + if (remain === 0) return 1; + return 0; + } + + // 用i和remain作为键,判断是否已经计算过这个子问题,如果已经计算,则直接返回结果,否则继续计算 + const key = i + "," + remain; + + if (memo.has(key)) { + return memo.get(key); + } + + // 分别递归计算加上和减去当前元素后能够得到目标值的方案数量,相加得到当前子问题的解 + const result = + dp(nums, i + 1, remain - nums[i]) + dp(nums, i + 1, remain + nums[i]); + + // 把计算结果存入备忘录 + memo.set(key, result); + + // 返回当前子问题的解 + return result; + }; + + // 从0开始遍历数组,计算从每个索引开始的子问题,最终得到问题的解 + return dp(nums, 0, target); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def findTargetSumWays(self, nums: List[int], target: int) -> int: + if len(nums) == 0: return 0 + # 调用备忘录函数 + return self.dp(nums, 0, target, {}) + + def dp(self, nums, i, remain, memo): + # base case + if i == len(nums): + if remain == 0: return 1 + return 0 + # 把它俩转成字符串才能作为哈希表的键 + key = str(i) + "," + str(remain) + # 避免重复计算 + if key in memo: + return memo[key] + # 还是穷举 + result = self.dp(nums, i + 1, remain - nums[i], memo) + self.dp(nums, i + 1, remain + nums[i], memo) + # 记入备忘录 + memo[key] = result + return result +``` + +https://leetcode.cn/problems/target-sum 的多语言解法👆 + +https://leetcode.cn/problems/the-maze 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool hasPath(vector>& maze, vector& start, vector& destination) { + int m = maze.size(), n = maze[0].size(); + // 方向数组,方便上下左右移动 + int dirs[4][2] = {{0,1},{0,-1},{1,0},{-1,0}}; + + // BFS 算法的队列和 visited 数组 + queue> q; + vector> visited(m, vector(n, false)); + // 加入起点 + q.push(start); + visited[start[0]][start[1]] = true; + // 启动 BFS 算法框架 + while (!q.empty()) { + vector cur = q.front(); + q.pop(); + // 向四个方向扩展 + for (auto dir : dirs) { + int x = cur[0], y = cur[1]; + // 和其他题目不同的是,这里一直走到墙,而不是只走一步 + while (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0) { + x += dir[0]; + y += dir[1]; + } + x -= dir[0]; + y -= dir[1]; + // 判断是否到达终点 + if (x == destination[0] && y == destination[1]) { + return true; + } + if (!visited[x][y]) { + visited[x][y] = true; + q.push({x, y}); + } + } + } + return false; + } +}; +``` + +```go +// by chatGPT (go) +import "fmt" + +func hasPath(maze [][]int, start []int, destination []int) bool { + m, n := len(maze), len(maze[0]) + // 方向数组,方便上下左右移动 + dirs := [][]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}} + + q := [][]int{start} + visited := make([][]bool, m) + for i := range visited { + visited[i] = make([]bool, n) + } + visited[start[0]][start[1]] = true + + // 启动 BFS 算法框架 + for len(q) > 0 { + cur := q[0] + q = q[1:] + // 向四个方向扩展 + for _, dir := range dirs { + x, y := cur[0], cur[1] + // 和其他题目不同的是,这里一直走到墙,而不是只走一步 + for x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0 { + x += dir[0] + y += dir[1] + } + x -= dir[0] + y -= dir[1] + // 判断是否到达终点 + if x == destination[0] && y == destination[1] { + return true + } + if !visited[x][y] { + visited[x][y] = true + q = append(q, []int{x, y}) + } + } + } + return false +} + +func main() { + maze := [][]int{ + {0,0,1,0,0}, + {0,0,0,0,0}, + {0,0,0,1,0}, + {1,1,0,1,1}, + {0,0,0,0,0}, + } + start := []int{0, 4} + destination := []int{4, 4} + fmt.Println(hasPath(maze, start, destination)) // Output: true +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean hasPath(int[][] maze, int[] start, int[] destination) { + int m = maze.length, n = maze[0].length; + // 方向数组,方便上下左右移动 + int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; + + // BFS 算法的队列和 visited 数组 + Queue q = new LinkedList<>(); + boolean[][] visited = new boolean[m][n]; + // 加入起点 + q.offer(start); + visited[start[0]][start[1]] = true; + // 启动 BFS 算法框架 + while (!q.isEmpty()) { + int[] cur = q.poll(); + // 向四个方向扩展 + for (int[] dir : dirs) { + int x = cur[0], y = cur[1]; + // 和其他题目不同的是,这里一直走到墙,而不是只走一步 + while (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0) { + x += dir[0]; + y += dir[1]; + } + x -= dir[0]; + y -= dir[1]; + // 判断是否到达终点 + if (x == destination[0] && y == destination[1]) { + return true; + } + if (!visited[x][y]) { + visited[x][y] = true; + q.offer(new int[]{x, y}); + } + } + } + return false; + } +} +``` + +```javascript +// by chatGPT (javascript) +var hasPath = function(maze, start, destination) { + var m = maze.length, n = maze[0].length; + // 方向数组,方便上下左右移动 + var dirs = [[0,1],[0,-1],[1,0],[-1,0]]; + + // BFS 算法的队列和 visited 数组 + var q = []; + var visited = new Array(m); + for (var i = 0; i < m; i++) { + visited[i] = new Array(n).fill(false); + } + // 加入起点 + q.push(start); + visited[start[0]][start[1]] = true; + // 启动 BFS 算法框架 + while (q.length > 0) { + var cur = q.shift(); + // 向四个方向扩展 + for (var i = 0; i < 4; i++) { + var dir = dirs[i]; + var x = cur[0], y = cur[1]; + // 和其他题目不同的是,这里一直走到墙,而不是只走一步 + while (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0) { + x += dir[0]; + y += dir[1]; + } + x -= dir[0]; + y -= dir[1]; + // 判断是否到达终点 + if (x == destination[0] && y == destination[1]) { + return true; + } + if (!visited[x][y]) { + visited[x][y] = true; + q.push([x, y]); + } + } + } + return false; +}; +``` + +```python +# by chatGPT (python) +from typing import List +from queue import Queue + +class Solution: + def hasPath(self, maze: List[List[int]], start: List[int], destination: List[int]) -> bool: + m, n = len(maze), len(maze[0]) # 获取矩阵的行和列 + dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)] # 四个方向 + + q = Queue() # BFS 算法的队列 + visited = [[False for _ in range(n)] for _ in range(m)] # visited 数组 + q.put(start) # 加入起点 + visited[start[0]][start[1]] = True # 设置该点已访问 + # 启动 BFS 算法框架 + while not q.empty(): + x, y = q.get() + # 向四个方向扩展 + for dir in dirs: + # 和其他题目不同的是,这里一直走到墙,而不是只走一步 + nr, nc = x, y + while 0 <= nr < m and 0 <= nc < n and not maze[nr][nc]: + nr += dir[0] + nc += dir[1] + nr -= dir[0] # 走过墙后需要回退一步 + nc -= dir[1] + # 判断是否到达终点 + if (nr, nc) == tuple(destination): + return True + if not visited[nr][nc]: + visited[nr][nc] = True # 设置该点已访问 + q.put((nr, nc)) + + return False +``` + +https://leetcode.cn/problems/the-maze 的多语言解法👆 + +https://leetcode.cn/problems/the-maze-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int shortestDistance(vector>& maze, vector& start, vector& destination) { + int m = maze.size(), n = maze[0].size(); + // 方向数组,方便上下左右移动 + vector> dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + // BFS 算法的队列和 visited 数组 + queue> q; + vector> visited(m, vector(n, false)); + // 加入起点 + q.push(start); + visited[start[0]][start[1]] = true; + // 启动 BFS 算法框架 + while (!q.empty()) { + vector cur = q.front(); + q.pop(); + // 向四个方向扩展 + for (auto dir : dirs) { + int x = cur[0], y = cur[1]; + int step = 0; + // 和其他题目不同的是,这里一直走到墙,而不是只走一步,同时要记录走过的步数 + while (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0) { + x += dir[0]; + y += dir[1]; + step++; + } + x -= dir[0]; + y -= dir[1]; + step--; + // 判断是否到达终点 + if (x == destination[0] && y == destination[1]) { + return step; + } + if (!visited[x][y]) { + visited[x][y] = true; + q.push({x, y}); + } + } + } + return -1; + } +}; +``` + +```go +// by chatGPT (go) +import "container/list" + +func shortestDistance(maze [][]int, start []int, destination []int) int { + m, n := len(maze), len(maze[0]) + // 方向数组,方便上下左右移动 + dirs := [][]int{{0,1},{0,-1},{1,0},{-1,0}} + + // BFS 算法的队列和 visited 数组 + q := list.New() + visited := make([][]bool, m) + for i := range visited { + visited[i] = make([]bool, n) + } + // 加入起点 + q.PushBack(start) + visited[start[0]][start[1]] = true + // 启动 BFS 算法框架 + for q.Len() != 0 { + cur := q.Front().Value.([]int) + q.Remove(q.Front()) + // 向四个方向扩展 + for _, dir := range dirs { + x, y := cur[0], cur[1] + step := 0 + // 和其他题目不同的是,这里一直走到墙,而不是只走一步,同时要记录走过的步数 + for x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0 { + x += dir[0] + y += dir[1] + step++ + } + x -= dir[0] + y -= dir[1] + step-- + // 判断是否到达终点 + if x == destination[0] && y == destination[1] { + return step + } + if !visited[x][y] { + visited[x][y] = true + q.PushBack([]int{x, y}) + } + } + } + return -1 +} +``` + +```java +// by labuladong (java) +class Solution { + public int shortestDistance(int[][] maze, int[] start, int[] destination) { + int m = maze.length, n = maze[0].length; + // 方向数组,方便上下左右移动 + int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; + + // BFS 算法的队列和 visited 数组 + Queue q = new LinkedList<>(); + boolean[][] visited = new boolean[m][n]; + // 加入起点 + q.offer(start); + visited[start[0]][start[1]] = true; + int step = 0; + // 启动 BFS 算法框架 + while (!q.isEmpty()) { + int[] cur = q.poll(); + // 向四个方向扩展 + for (int[] dir : dirs) { + int x = cur[0], y = cur[1]; + // 和其他题目不同的是,这里一直走到墙,而不是只走一步,同时要记录走过的步数 + while (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0) { + x += dir[0]; + y += dir[1]; + step++; + } + x -= dir[0]; + y -= dir[1]; + step--; + // 判断是否到达终点 + if (x == destination[0] && y == destination[1]) { + return step; + } + if (!visited[x][y]) { + visited[x][y] = true; + q.offer(new int[]{x, y}); + } + } + } + return -1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var shortestDistance = function(maze, start, destination) { + var m = maze.length, n = maze[0].length; + // 方向数组,方便上下左右移动 + var dirs = [[0,1],[0,-1],[1,0],[-1,0]]; + + // BFS 算法的队列和 visited 数组 + var q = []; + var visited = new Array(m); + for (var i = 0; i < m; i++) { + visited[i] = new Array(n).fill(false); + } + // 加入起点 + q.push(start); + visited[start[0]][start[1]] = true; + // 启动 BFS 算法框架 + while (q.length > 0) { + var cur = q.shift(); + // 向四个方向扩展 + for (var dir of dirs) { + var x = cur[0], y = cur[1]; + var step = 0; + // 和其他题目不同的是,这里一直走到墙,而不是只走一步,同时要记录走过的步数 + while (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0) { + x += dir[0]; + y += dir[1]; + step++; + } + x -= dir[0]; + y -= dir[1]; + step--; + // 判断是否到达终点 + if (x == destination[0] && y == destination[1]) { + return step; + } + if (!visited[x][y]) { + visited[x][y] = true; + q.push([x, y]); + } + } + } + return -1; +}; +``` + +```python +# by chatGPT (python) +from queue import Queue + +class Solution: + def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]) -> int: + m, n = len(maze), len(maze[0]) + + # 方向数组,方便上下左右移动 + dirs = [(0,1), (0,-1), (1,0), (-1,0)] + + # BFS 算法的队列和 visited 数组 + q = Queue() + visited = [[False for _ in range(n)] for _ in range(m)] + # 加入起点 + q.put(start) + visited[start[0]][start[1]] = True + # 启动 BFS 算法框架 + while not q.empty(): + cur = q.get() + # 向四个方向扩展 + for dir in dirs: + x, y = cur[0], cur[1] + step = 0 + # 和其他题目不同的是,这里一直走到墙,而不是只走一步,同时要记录走过的步数 + while x >= 0 and x < m and y >= 0 and y < n and maze[x][y] == 0: + x += dir[0] + y += dir[1] + step += 1 + x -= dir[0] + y -= dir[1] + step -= 1 + # 判断是否到达终点 + if x == destination[0] and y == destination[1]: + return step + if not visited[x][y]: + visited[x][y] = True + q.put((x, y)) + return -1 +``` + +https://leetcode.cn/problems/the-maze-ii 的多语言解法👆 + +https://leetcode.cn/problems/ti-huan-kong-ge-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string replaceSpace(string s) { + string ans; + for (char c : s) { + if (c == ' ') { + ans += "%20"; + } else { + ans += c; + } + } + return ans; + } +}; +``` + +```go +// by chatGPT (go) +/** + * @author ylx + * @date 2021/2/2 19:19 + * @description Go 实现的 LeetCode 剑指 Offer 05. 替换空格 + */ +func replaceSpace(s string) string { + var res strings.Builder + for i := 0; i < len(s); i++ { + if s[i] == ' ' { + res.WriteString("%20") + } else { + res.WriteByte(s[i]) + } + } + return res.String() +} +``` + +```java +// by labuladong (java) +class Solution { + public String replaceSpace(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == ' ') { + sb.append("%20"); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var replaceSpace = function(s) { + var sb = []; + for (var i = 0; i < s.length; i++) { + var c = s.charAt(i); + if (c === ' ') { + sb.push('%20'); + } else { + sb.push(c); + } + } + return sb.join(''); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def replaceSpace(self, s: str) -> str: + sb = [] + for c in s: + if c == ' ': + sb.append('%20') + else: + sb.append(c) + return ''.join(sb) +``` + +https://leetcode.cn/problems/ti-huan-kong-ge-lcof 的多语言解法👆 + +https://leetcode.cn/problems/top-k-frequent-elements 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector topKFrequent(vector& nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + unordered_map valToFreq; + for (int v : nums) { + valToFreq[v]++; + } + + priority_queue, vector>, greater<>> pq; + for (auto& [val, freq] : valToFreq) { + pq.emplace(freq, val); + if (pq.size() > k) { + pq.pop(); + } + } + + vector res(k); + int i = k - 1; + while (!pq.empty()) { + res[i] = pq.top().second; + pq.pop(); + i--; + } + + return res; + } +}; + +class Solution2 { +public: + vector topKFrequent(vector& nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + unordered_map valToFreq; + for (int v : nums) { + valToFreq[v]++; + } + + // 频率 -> 这个频率有哪些元素 + vector> freqToVals(nums.size() + 1); + for (auto& [val, freq] : valToFreq) { + freqToVals[freq].push_back(val); + } + + vector res(k); + int p = 0; + // freqToVals 从后往前存储着出现最多的元素 + for (int i = freqToVals.size() - 1; i > 0; i--) { + for (int j = 0; j < freqToVals[i].size(); j++) { + res[p] = freqToVals[i][j]; + p++; + if (p == k) { + return res; + } + } + } + + return res; + } +}; +``` + +```go +// by chatGPT (go) +// 用优先级队列解决这道题 +func topKFrequent(nums []int, k int) []int { + // nums 中的元素 -> 该元素出现的频率 + valToFreq := make(map[int]int) + for _, v := range nums { + valToFreq[v]++ + } + + // 定义优先队列,队列按照键值对中的值(元素出现频率)从小到大排序。 + pq := priorityQueue{} + for key, value := range valToFreq { + pq = pq.Push(MapEntry{key, value}) + if len(pq) > k { + pq.Pop() + } + } + + res := make([]int, k) + for i := k - 1; i >= 0; i-- { + // res 数组中存储前 k 个最大元素 + res[i] = pq.Pop().key + } + + return res +} + +// 定义优先级队列 +type MapEntry struct { + key int + value int +} +type priorityQueue []MapEntry + +func (pq priorityQueue) Len() int { return len(pq) } +func (pq priorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] } +func (pq priorityQueue) Less(i, j int) bool { + return pq[i].value < pq[j].value +} +func (pq priorityQueue) Push(x interface{}) priorityQueue { + item := x.(MapEntry) + return append(pq, item) +} +func (pq priorityQueue) Pop() MapEntry { + item := pq[pq.Len()-1] + pq = pq[:pq.Len()-1] + return item +} + +// 用计数排序的方法解决这道题 +func topKFrequent2(nums []int, k int) []int { + // nums 中的元素 -> 该元素出现的频率 + valToFreq := make(map[int]int) + for _, v := range nums { + valToFreq[v]++ + } + + // 频率 -> 这个频率有哪些元素 + freqToVals := make([][]int, len(nums)+1) + for val, freq := range valToFreq { + if _, ok := freqToVals[freq]; !ok { + freqToVals[freq] = []int{} + } + freqToVals[freq] = append(freqToVals[freq], val) + } + + res := make([]int, 0, k) + // freqToVals 从后往前存储着出现最多的元素 + for i := len(freqToVals) - 1; i > 0; i-- { + if len(freqToVals[i]) == 0 { + continue + } + res = append(res, freqToVals[i]...) + if len(res) >= k { + res = res[:k] + break + } + } + + return res +} +``` + +```java +// by labuladong (java) +// 用优先级队列解决这道题 +class Solution { + public int[] topKFrequent(int[] nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + HashMap valToFreq = new HashMap<>(); + for (int v : nums) { + valToFreq.put(v, valToFreq.getOrDefault(v, 0) + 1); + } + + PriorityQueue> + pq = new PriorityQueue<>((entry1, entry2) -> { + // 队列按照键值对中的值(元素出现频率)从小到大排序 + return entry1.getValue().compareTo(entry2.getValue()); + }); + + for (Map.Entry entry : valToFreq.entrySet()) { + pq.offer(entry); + if (pq.size() > k) { + // 弹出最小元素,维护队列内是 k 个频率最大的元素 + pq.poll(); + } + } + + int[] res = new int[k]; + for (int i = k - 1; i >= 0; i--) { + // res 数组中存储前 k 个最大元素 + res[i] = pq.poll().getKey(); + } + + return res; + } +} + +// 用计数排序的方法解决这道题 +class Solution2 { + public int[] topKFrequent(int[] nums, int k) { + // nums 中的元素 -> 该元素出现的频率 + HashMap valToFreq = new HashMap<>(); + for (int v : nums) { + valToFreq.put(v, valToFreq.getOrDefault(v, 0) + 1); + } + + // 频率 -> 这个频率有哪些元素 + ArrayList[] freqToVals = new ArrayList[nums.length + 1]; + for (int val : valToFreq.keySet()) { + int freq = valToFreq.get(val); + if (freqToVals[freq] == null) { + freqToVals[freq] = new ArrayList<>(); + } + freqToVals[freq].add(val); + } + + int[] res = new int[k]; + int p = 0; + // freqToVals 从后往前存储着出现最多的元素 + for (int i = freqToVals.length - 1; i > 0; i--) { + ArrayList valList = freqToVals[i]; + if (valList == null) continue; + for (int j = 0; j < valList.size(); j++) { + // 将出现次数最多的 k 个元素装入 res + res[p] = valList.get(j); + p++; + if (p == k) { + return res; + } + } + } + + return null; + } +} +``` + +```javascript +// by chatGPT (javascript) +var topKFrequent = function(nums, k) { + // nums 中的元素 -> 该元素出现的频率 + const valToFreq = new Map(); + for (let v of nums) { + valToFreq.set(v, valToFreq.get(v) ? valToFreq.get(v) + 1: 1); + } + + const pq = new PriorityQueue((a, b) => a.value < b.value); + + for (let [key, value] of valToFreq) { + pq.offer({key, value}); + if (pq.size() > k) { + pq.poll(); + } + } + + const res = []; + for (let i = 0; i < k; i++) { + res.push(pq.poll().key); + } + return res; +}; +``` + +```python +# by chatGPT (python) +# 用优先级队列解决这道题 +class Solution: + def topKFrequent(self, nums: List[int], k: int) -> List[int]: + # nums 中的元素 -> 该元素出现的频率 + valToFreq = {} + for v in nums: + valToFreq[v] = valToFreq.get(v, 0) + 1 + + pq = [] + # 按照键值对中的值(元素出现频率)从小到大排序 + for entry in valToFreq.items(): + heapq.heappush(pq, (entry[1], entry[0])) + if len(pq) > k: + # 弹出最小元素,维护队列内是 k 个频率最大的元素 + heapq.heappop(pq) + + res = [] + for i in range(k - 1, -1, -1): + # res 数组中存储前 k 个最大元素 + res.append(heapq.heappop(pq)[1]) + + return res[::-1] + +# 用计数排序的方法解决这道题 +class Solution2: + def topKFrequent(self, nums: List[int], k: int) -> List[int]: + # nums 中的元素 -> 该元素出现的频率 + valToFreq = {} + for v in nums: + valToFreq[v] = valToFreq.get(v, 0) + 1 + + # 频率 -> 这个频率有哪些元素 + freqToVals = [[] for _ in range(len(nums) + 1)] + for val, freq in valToFreq.items(): + freqToVals[freq].append(val) + + res = [] + # freqToVals 从后往前存储着出现最多的元素 + for i in range(len(freqToVals) - 1, 0, -1): + valList = freqToVals[i] + for j in range(len(valList)): + # 将出现次数最多的 k 个元素装入 res + res.append(valList[j]) + if len(res) == k: + return res + + return res +``` + +https://leetcode.cn/problems/top-k-frequent-elements 的多语言解法👆 + +https://leetcode.cn/problems/top-k-frequent-words 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector topKFrequent(vector& words, int k) { + // 字符串 -> 该字符串出现的频率 + unordered_map wordToFreq; + for (string word : words) { + wordToFreq[word] += 1; + } + + // 重载 lambda 表达式,处理相同频率的集合按字典序优先级 + auto cmp = [&](const pair& a, const pair& b) { + if (a.second == b.second) { + return a.first < b.first; + } + return a.second > b.second; + }; + priority_queue, vector>, decltype(cmp)> pq(cmp); + + // 维护出现频率最多的 k 个字符串 + for (auto it : wordToFreq) { + pq.emplace(it.first, it.second); + if (pq.size() > k) { + pq.pop(); + } + } + + // 把出现次数最多的 k 个字符串返回 + vector res(k, ""); + for (int i = k - 1; i >= 0; --i) { + res[i] = pq.top().first; + pq.pop(); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func topKFrequent(words []string, k int) []string { + // 字符串 -> 该字符串出现的频率 + wordToFreq := make(map[string]int) + for _, word := range words { + wordToFreq[word]++ + } + + pq := priorityQueue{} + heap.Init(&pq) + + // entry结构体数组 + type entry struct { + key string + value int + } + + // 大顶堆函数 + heapFunc := func(a, b *entry) bool { + // 注意这里,如果出现频率相同,按照字符串字典序排序 + if a.value == b.value { + return a.key > b.key + } + // 队列按照字符串出现频率从小到大排序 + return a.value < b.value + } + + // 维护出现频率最多的 k 个单词 + for key, value := range wordToFreq { + en := &entry{key, value} + if pq.Len() == k { + if heapFunc(pq[0], en) { + heap.Pop(&pq) + heap.Push(&pq, en) + } + } else { + heap.Push(&pq, en) + } + } + + // 把出现次数最多的 k 个字符串返回 + res := make([]string, k) + for i := k - 1; i >= 0; i-- { + res[i] = heap.Pop(&pq).(*entry).key + } + return res +} + +// 定义堆排序函数 +type priorityQueue []*entry + +func (pq priorityQueue) Len() int { return len(pq) } + +func (pq priorityQueue) Less(i, j int) bool { + // 注意这里,仿佛要反着来 + return pq[i].value > pq[j].value +} + +func (pq priorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] } + +func (pq *priorityQueue) Push(x interface{}) { + // 注意类型转换 + item := x.(*entry) + *pq = append(*pq, item) +} + +func (pq *priorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} +``` + +```java +// by labuladong (java) +class Solution { + public List topKFrequent(String[] words, int k) { + // 字符串 -> 该字符串出现的频率 + HashMap wordToFreq = new HashMap<>(); + for (String word : words) { + wordToFreq.put(word, wordToFreq.getOrDefault(word, 0) + 1); + } + + PriorityQueue> pq = new PriorityQueue<>( + (entry1, entry2) -> { + if (entry1.getValue().equals(entry2.getValue())) { + // 如果出现频率相同,按照字符串字典序排序 + return entry2.getKey().compareTo(entry1.getKey()); + } + // 队列按照字符串出现频率从小到大排序 + return entry1.getValue().compareTo(entry2.getValue()); + }); + + // 按照字符串频率升序排序 + for (Map.Entry entry : wordToFreq.entrySet()) { + pq.offer(entry); + if (pq.size() > k) { + // 维护出现频率最多的 k 个单词 + pq.poll(); + } + } + + // 把出现次数最多的 k 个字符串返回 + LinkedList res = new LinkedList<>(); + while (!pq.isEmpty()) { + res.addFirst(pq.poll().getKey()); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var topKFrequent = function(words, k) { + // 字符串 -> 该字符串出现的频率 + let wordToFreq = new Map(); + for (let word of words) { + wordToFreq.set(word, wordToFreq.get(word) + 1 || 1); + } + + let pq = new PriorityQueue((a, b) => { + if (a.freq === b.freq) { + // 如果出现频率相同,按照字符串字典序排序 + return b.word.localeCompare(a.word); + } + // 队列按照字符串出现频率从小到大排序 + return a.freq - b.freq; + }); + + // 按照字符串频率升序排序 + for (let [word, freq] of wordToFreq.entries()) { + pq.offer({ word, freq }); + if (pq.size > k) { + // 维护出现频率最多的 k 个单词 + pq.poll(); + } + } + + // 把出现次数最多的 k 个字符串返回 + let res = []; + while (!pq.isEmpty()) { + res.push(pq.poll().word); + } + return res.reverse(); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def topKFrequent(self, words: List[str], k: int) -> List[str]: + # 字符串 -> 该字符串出现的频率 + wordToFreq = {} + for word in words: + wordToFreq[word] = wordToFreq.get(word, 0) + 1 + + pq = [] + for word, freq in wordToFreq.items(): + # 存储负值,让小根堆变成大根堆 + pq.append((-freq, word)) + + # 构建小根堆 + heapq.heapify(pq) + + # 取出前k个高频单词 + res = [] + for _ in range(k): + freq, word = heapq.heappop(pq) + res.append(word) + + return res +``` + +https://leetcode.cn/problems/top-k-frequent-words 的多语言解法👆 + +https://leetcode.cn/problems/transpose-matrix 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector> transpose(vector>& matrix) { + int m = matrix.size(), n = matrix[0].size(); + // 转置矩阵的长和宽颠倒 + vector> res(n, vector(m)); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + res[j][i] = matrix[i][j]; + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +// leetcode format +func transpose(matrix [][]int) [][]int { + m, n := len(matrix), len(matrix[0]) + // 转置矩阵的长和宽颠倒 + res := make([][]int, n) + for i := range res { + res[i] = make([]int, m) + } + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + res[j][i] = matrix[i][j] + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + public int[][] transpose(int[][] matrix) { + int m = matrix.length, n = matrix[0].length; + // 转置矩阵的长和宽颠倒 + int[][] res = new int[n][m]; + for (int i = 0; i < m; i++) + for (int j = 0; j < n; j++) { + res[j][i] = matrix[i][j]; + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var transpose = function(matrix) { + var m = matrix.length, n = matrix[0].length; + // 转置矩阵的长和宽颠倒 + var res = new Array(n).fill().map(() => new Array(m).fill(0)); + for (var i = 0; i < m; i++) + for (var j = 0; j < n; j++) { + res[j][i] = matrix[i][j]; + } + return res; +} +``` + +```python +# by chatGPT (python) +class Solution: + def transpose(self, matrix: List[List[int]]) -> List[List[int]]: + m, n = len(matrix), len(matrix[0]) #转置矩阵的长和宽颠倒 + res = [[0] * m for _ in range(n)] + for i in range(m): + for j in range(n): + res[j][i] = matrix[i][j] + return res +``` + +https://leetcode.cn/problems/transpose-matrix 的多语言解法👆 + +https://leetcode.cn/problems/trapping-rain-water 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int trap(vector& height) { + if (height.empty()) { + return 0; + } + int n = height.size(); + int res = 0; + // 数组充当备忘录 + vector l_max(n); + vector r_max(n); + // 初始化 base case + l_max[0] = height[0]; + r_max[n - 1] = height[n - 1]; + // 从左向右计算 l_max + for (int i = 1; i < n; i++) { + l_max[i] = max(height[i], l_max[i - 1]); + } + // 从右向左计算 r_max + for (int i = n - 2; i >= 0; i--) { + r_max[i] = max(height[i], r_max[i + 1]); + } + // 计算答案 + for (int i = 1; i < n - 1; i++) { + res += min(l_max[i], r_max[i]) - height[i]; + } + /** + ![](../pictures/接雨水/1.jpg) + */ + return res; + } +}; +``` + +```go +// by chatGPT (go) +func trap(height []int) int { + if len(height) == 0 { + return 0 + } + n := len(height) + res := 0 + // 数组充当备忘录 + l_max := make([]int, n) + r_max := make([]int, n) + // 初始化 base case + l_max[0] = height[0] + r_max[n - 1] = height[n - 1] + // 从左向右计算 l_max + for i := 1; i < n; i++ { + l_max[i] = max(height[i], l_max[i - 1]) + } + // 从右向左计算 r_max + for i := n - 2; i >= 0; i-- { + r_max[i] = max(height[i], r_max[i + 1]) + } + // 计算答案 + for i := 1; i < n - 1; i++ { + res += min(l_max[i], r_max[i]) - height[i] + /** + ![](../pictures/接雨水/1.jpg) + */ + } + return res; +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int trap(int[] height) { + if (height.length == 0) { + return 0; + } + int n = height.length; + int res = 0; + // 数组充当备忘录 + int[] l_max = new int[n]; + int[] r_max = new int[n]; + // 初始化 base case + l_max[0] = height[0]; + r_max[n - 1] = height[n - 1]; + // 从左向右计算 l_max + for (int i = 1; i < n; i++) + l_max[i] = Math.max(height[i], l_max[i - 1]); + // 从右向左计算 r_max + for (int i = n - 2; i >= 0; i--) + r_max[i] = Math.max(height[i], r_max[i + 1]); + // 计算答案 + for (int i = 1; i < n - 1; i++) + res += Math.min(l_max[i], r_max[i]) - height[i]; + /** + ![](../pictures/接雨水/1.jpg) + */ + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var trap = function(height) { + if (height.length == 0) { + return 0; + } + var n = height.length; + var res = 0; + // 数组充当备忘录 + var l_max = new Array(n); + var r_max = new Array(n); + // 初始化 base case + l_max[0] = height[0]; + r_max[n - 1] = height[n - 1]; + // 从左向右计算 l_max + for (var i = 1; i < n; i++) { + l_max[i] = Math.max(height[i], l_max[i - 1]); + } + // 从右向左计算 r_max + for (var i = n - 2; i >= 0; i--) { + r_max[i] = Math.max(height[i], r_max[i + 1]); + } + // 计算答案 + for (var i = 1; i < n - 1; i++) { + res += Math.min(l_max[i], r_max[i]) - height[i]; + } + /** + ![](../pictures/接雨水/1.jpg) + */ + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def trap(self, height: List[int]) -> int: + if not height: + return 0 + n = len(height) + res = 0 + # 数组充当备忘录 + l_max = [0] * n + r_max = [0] * n + # 初始化 base case + l_max[0] = height[0] + r_max[n - 1] = height[n - 1] + # 从左向右计算 l_max + for i in range(1, n): + l_max[i] = max(height[i], l_max[i - 1]) + # 从右向左计算 r_max + for i in range(n - 2, -1, -1): + r_max[i] = max(height[i], r_max[i + 1]) + # 计算答案 + for i in range(1, n - 1): + res += min(l_max[i], r_max[i]) - height[i] + """ + extend up -300 + ![](../pictures/接雨水/1.jpg) + """ + return res +``` + +https://leetcode.cn/problems/trapping-rain-water 的多语言解法👆 + +https://leetcode.cn/problems/two-sum 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector twoSum(vector& nums, int target) { + // 维护 val -> index 的映射 + unordered_map valToIndex; + for (int i = 0; i < nums.size(); i++) { + // 查表,看看是否有能和 nums[i] 凑出 target 的元素 + int need = target - nums[i]; + if (valToIndex.count(need)) { + return vector{valToIndex[need], i}; + } + // 存入 val -> index 的映射 + valToIndex[nums[i]] = i; + } + return vector{}; + } +}; +``` + +```go +// by chatGPT (go) +func twoSum(nums []int, target int) []int { + // 维护 val -> index 的映射 + valToIndex := make(map[int]int) + + for i, num := range nums { + // 查表,看看是否有能和 nums[i] 凑出 target 的元素 + need := target - num + if valToIndex[need] != 0 { + return []int{valToIndex[need] - 1, i} + } + // 存入 val -> index 的映射 + valToIndex[num] = i + 1 + } + + return nil +} +``` + +```java +// by labuladong (java) +class Solution { + public int[] twoSum(int[] nums, int target) { + // 维护 val -> index 的映射 + HashMap valToIndex = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + // 查表,看看是否有能和 nums[i] 凑出 target 的元素 + int need = target - nums[i]; + if (valToIndex.containsKey(need)) { + return new int[]{valToIndex.get(need), i}; + } + // 存入 val -> index 的映射 + valToIndex.put(nums[i], i); + } + return null; + } +} +``` + +```javascript +// by chatGPT (javascript) +var twoSum = function(nums, target) { + // 维护 val -> index 的映射 + var valToIndex = new Map(); + for (var i = 0; i < nums.length; i++) { + // 查表,看看是否有能和 nums[i] 凑出 target 的元素 + var need = target - nums[i]; + if (valToIndex.has(need)) { + return [valToIndex.get(need), i]; + } + // 存入 val -> index 的映射 + valToIndex.set(nums[i], i); + } + return null; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def twoSum(self, nums: List[int], target: int) -> List[int]: + # 维护 val -> index 的映射 + valToIndex = {} + for i in range(len(nums)): + # 查表,看看是否有能和 nums[i] 凑出 target 的元素 + need = target - nums[i] + if need in valToIndex: + return [valToIndex[need], i] + # 存入 val -> index 的映射 + valToIndex[nums[i]] = i + return [] +``` + +https://leetcode.cn/problems/two-sum 的多语言解法👆 + +https://leetcode.cn/problems/two-sum-less-than-k 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int twoSumLessThanK(vector& nums, int k) { + // 数组双指针一般都要先排序 + sort(nums.begin(), nums.end()); + // 左右指针技巧 + int lo = 0, hi = nums.size() - 1; + int sum = -1; + while (lo < hi) { + if (nums[lo] + nums[hi] < k) { + // 比目标值 k 小,则右移左指针 + sum = max(sum, nums[lo] + nums[hi]); + lo++; + } else { + // 比目标值 k 大,则左移右指针 + hi--; + } + } + return sum; + } +}; +``` + +```go +// by chatGPT (go) +// +import "sort" + +// TwoSumLessThanK 函数返回 nums 数组中任意两个数的最大和, +// 但不超过 k 的和 +func twoSumLessThanK(nums []int, k int) int { + // 数组双指针一般都要先排序 + sort.Ints(nums) + + // 左右指针技巧 + lo, hi := 0, len(nums)-1 + sum := -1 + + for lo < hi { + if nums[lo]+nums[hi] < k { + // 比目标值 k 小,则右移左指针 + sum = max(sum, nums[lo]+nums[hi]) + lo++ + } else { + // 比目标值 k 大,则左移右指针 + hi-- + } + } + + return sum +} + +// 辅助函数 max 返回两个整数中的较大值 +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int twoSumLessThanK(int[] nums, int k) { + // 数组双指针一般都要先排序 + Arrays.sort(nums); + // 左右指针技巧 + int lo = 0, hi = nums.length - 1; + int sum = -1; + while (lo < hi) { + if (nums[lo] + nums[hi] < k) { + // 比目标值 k 小,则右移左指针 + sum = Math.max(sum, nums[lo] + nums[hi]); + lo++; + } else { + // 比目标值 k 大,则左移右指针 + hi--; + } + } + return sum; + } +} +``` + +```javascript +// by chatGPT (javascript) +var twoSumLessThanK = function(nums, k) { + // 数组双指针一般都要先排序 + nums.sort(function(a, b){return a - b}); + // 左右指针技巧 + var lo = 0, hi = nums.length - 1; + var sum = -1; + while (lo < hi) { + if (nums[lo] + nums[hi] < k) { + // 比目标值 k 小,则右移左指针 + sum = Math.max(sum, nums[lo] + nums[hi]); + lo++; + } else { + // 比目标值 k 大,则左移右指针 + hi--; + } + } + return sum; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def twoSumLessThanK(self, nums: List[int], k: int) -> int: + # 数组双指针一般都要先排序 + nums.sort() + # 左右指针技巧 + lo, hi = 0, len(nums) - 1 + sum = -1 + while lo < hi: + if nums[lo] + nums[hi] < k: + # 比目标值 k 小,则右移左指针 + sum = max(sum, nums[lo] + nums[hi]) + lo += 1 + else: + # 比目标值 k 大,则左移右指针 + hi -= 1 + return sum +``` + +https://leetcode.cn/problems/two-sum-less-than-k 的多语言解法👆 + +https://leetcode.cn/problems/uUsW3B 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + + vector>res; + vector> combine(int n, int k) { + if (k <= 0 || n <= 0) return res; + vector track; + backtrack(n, k, 1, track); + return res; + } + + void backtrack(int n, int k, int start, vector& track) { + // 到达树的底部 + if (k == track.size()) { + res.push_back(track); + return; + } + // 注意 i 从 start 开始递增 + for (int i = start; i <= n; i++) { + // 做选择 + track.push_back(i); + backtrack(n, k, i + 1, track); + // 撤销选择 + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +func combine(n int, k int) [][]int { + res := make([][]int, 0) + if k <= 0 || n <= 0 { + return res + } + track := make([]int, 0) + backtrack(n, k, 1, track, &res) + return res +} + +func backtrack(n, k, start int, track []int, res *[][]int) { + // 到达树的底部 + if k == len(track) { + temp := make([]int, len(track)) + copy(temp, track) + *res = append(*res, temp) + return + } + // 注意 i 从 start 开始递增 + for i := start; i <= n; i++ { + // 做选择 + track = append(track, i) + backtrack(n, k, i+1, track, res) + // 撤销选择 + track = track[:len(track)-1] + } +} +``` + +```java +// by chatGPT (java) +class Solution { + List> res = new ArrayList<>(); + + public List> combine(int n, int k) { + if (k <= 0 || n <= 0) return res; + List track = new ArrayList<>(); + backtrack(n, k, 1, track); + return res; + } + + private void backtrack(int n, int k, int start, List track) { + // 到达树的底部 + if (k == track.size()) { + res.add(new ArrayList<>(track)); + return; + } + // 注意 i 从 start 开始递增 + for (int i = start; i <= n; i++) { + // 做选择 + track.add(i); + backtrack(n, k, i + 1, track); + // 撤销选择 + track.remove(track.size() - 1); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var combine = function(n, k) { + let res = []; + if (k <= 0 || n <= 0) return res; + let track = []; + // 到达树的底部 + const backtrack = function(n, k, start, track) { + if (k === track.length) { + res.push([...track]); + return; + } + // 注意 i 从 start 开始递增 + for (let i = start; i <= n; i++) { + // 做选择 + track.push(i); + backtrack(n, k, i + 1, track); + // 撤销选择 + track.pop(); + } + }; + backtrack(n, k, 1, track); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def combine(self, n: int, k: int) -> List[List[int]]: + res = [] + if k <= 0 or n <= 0: + return res + def backtrack(n: int, k: int, start: int, track: List[int]) -> None: + # 到达树的底部 + if k == len(track): + res.append(track) + return + # 注意 i 从 start 开始递增 + for i in range(start, n + 1): + # 做选择 + track.append(i) + backtrack(n, k, i + 1, track[:]) + # 撤销选择 + track.pop() + backtrack(n, k, 1, []) + return res +``` + +https://leetcode.cn/problems/uUsW3B 的多语言解法👆 + +https://leetcode.cn/problems/ugly-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isUgly(int n) { + if (n <= 0) return false; + // 如果 n 是丑数,分解因子应该只有 2, 3, 5 + while (n % 2 == 0) n /= 2; + while (n % 3 == 0) n /= 3; + while (n % 5 == 0) n /= 5; + return n == 1; + } +}; +``` + +```go +// by chatGPT (go) +func isUgly(n int) bool { + if n <= 0 { + return false + } + // 如果 n 是丑数,分解因子应该只有 2, 3, 5 + for n % 2 == 0 { + n /= 2 + } + for n % 3 == 0 { + n /= 3 + } + for n % 5 == 0 { + n /= 5 + } + return n == 1 +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isUgly(int n) { + if (n <= 0) return false; + // 如果 n 是丑数,分解因子应该只有 2, 3, 5 + while (n % 2 == 0) n /= 2; + while (n % 3 == 0) n /= 3; + while (n % 5 == 0) n /= 5; + return n == 1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isUgly = function(n) { + if (n <= 0) return false; + // 如果 n 是丑数,分解因子应该只有 2, 3, 5 + while (n % 2 === 0) n /= 2; + while (n % 3 === 0) n /= 3; + while (n % 5 === 0) n /= 5; + return n === 1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isUgly(self, n: int) -> bool: + if n <= 0: + return False + # 如果 n 是丑数,分解因子应该只有 2, 3, 5 + while n % 2 == 0: + n //= 2 + while n % 3 == 0: + n //= 3 + while n % 5 == 0: + n //= 5 + return n == 1 +``` + +https://leetcode.cn/problems/ugly-number 的多语言解法👆 + +https://leetcode.cn/problems/ugly-number-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int nthUglyNumber(int n) { + // 可以理解为三个指向有序链表头结点的指针 + int p2 = 1, p3 = 1, p5 = 1; + // 可以理解为三个有序链表的头节点的值 + int product2 = 1, product3 = 1, product5 = 1; + // 可以理解为最终合并的有序链表(结果链表) + vector ugly(n + 1, 0); + // 可以理解为结果链表上的指针 + int p = 1; + + // 开始合并三个有序链表 + while (p <= n) { + // 取三个链表的最小结点 + int min_val = min(min(product2, product3), product5); + // 接到结果链表上 + ugly[p] = min_val; + p++; + // 前进对应有序链表上的指针 + if (min_val == product2) { + product2 = 2 * ugly[p2]; + p2++; + } + if (min_val == product3) { + product3 = 3 * ugly[p3]; + p3++; + } + if (min_val == product5) { + product5 = 5 * ugly[p5]; + p5++; + } + } + // 返回第 n 个丑数 + return ugly[n]; + } +}; +``` + +```go +// by chatGPT (go) +func nthUglyNumber(n int) int { + // 可以理解为三个指向有序链表头结点的指针 + p2, p3, p5 := 1, 1, 1 + // 可以理解为三个有序链表的头节点的值 + product2, product3, product5 := 1, 1, 1 + // 可以理解为最终合并的有序链表(结果链表) + ugly := make([]int, n+1) + // 可以理解为结果链表上的指针 + p := 1 + + // 开始合并三个有序链表 + for p <= n { + // 取三个链表的最小结点 + min := min(product2, product3, product5) + // 接到结果链表上 + ugly[p] = min + p++ + // 前进对应有序链表上的指针 + if min == product2 { + product2 = 2 * ugly[p2] + p2++ + } + if min == product3 { + product3 = 3 * ugly[p3] + p3++ + } + if min == product5 { + product5 = 5 * ugly[p5] + p5++ + } + } + // 返回第 n 个丑数 + return ugly[n] +} + +// 取三个数的最小值 +func min(i, j, k int) int { + if i < j { + if i < k { + return i + } else { + return k + } + } else { + if j < k { + return j + } else { + return k + } + } +} +``` + +```java +// by labuladong (java) +class Solution { + public int nthUglyNumber(int n) { + // 可以理解为三个指向有序链表头结点的指针 + int p2 = 1, p3 = 1, p5 = 1; + // 可以理解为三个有序链表的头节点的值 + int product2 = 1, product3 = 1, product5 = 1; + // 可以理解为最终合并的有序链表(结果链表) + int[] ugly = new int[n + 1]; + // 可以理解为结果链表上的指针 + int p = 1; + + // 开始合并三个有序链表 + while (p <= n) { + // 取三个链表的最小结点 + int min = Math.min(Math.min(product2, product3), product5); + // 接到结果链表上 + ugly[p] = min; + p++; + // 前进对应有序链表上的指针 + if (min == product2) { + product2 = 2 * ugly[p2]; + p2++; + } + if (min == product3) { + product3 = 3 * ugly[p3]; + p3++; + } + if (min == product5) { + product5 = 5 * ugly[p5]; + p5++; + } + } + // 返回第 n 个丑数 + return ugly[n]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var nthUglyNumber = function(n) { + // 可以理解为三个指向有序链表头结点的指针 + let p2 = 1, p3 = 1, p5 = 1; + // 可以理解为三个有序链表的头节点的值 + let product2 = 1, product3 = 1, product5 = 1; + // 可以理解为最终合并的有序链表(结果链表) + let ugly = new Array(n + 1); + // 可以理解为结果链表上的指针 + let p = 1; + + // 开始合并三个有序链表 + while (p <= n) { + // 取三个链表的最小结点 + let min = Math.min(Math.min(product2, product3), product5); + // 接到结果链表上 + ugly[p] = min; + p++; + // 前进对应有序链表上的指针 + if (min === product2) { + product2 = 2 * ugly[p2]; + p2++; + } + if (min === product3) { + product3 = 3 * ugly[p3]; + p3++; + } + if (min === product5) { + product5 = 5 * ugly[p5]; + p5++; + } + } + // 返回第 n 个丑数 + return ugly[n]; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def nthUglyNumber(self,n: int) -> int: + # 三个指向有序链表头结点的指针 + p2,p3,p5 = 1,1,1 + # 三个有序链表的头节点的值 + product2, product3, product5 = 1, 1, 1 + # 最终合并的有序链表(结果链表) + ugly = [0] * (n + 1) + # 结果链表上的指针 + p = 1 + + # 开始合并三个有序链表 + while p <= n: + # 取三个链表的最小结点 + minv = min(product2, product3, product5) + # 接到结果链表上 + ugly[p] = minv + p += 1 + # 前进对应有序链表上的指针 + if minv == product2: + product2 = 2 * ugly[p2] + p2 += 1 + if minv == product3: + product3 = 3 * ugly[p3] + p3 += 1 + if minv == product5: + product5 = 5 * ugly[p5] + p5 += 1 + + # 返回第 n 个丑数 + return ugly[n] +``` + +https://leetcode.cn/problems/ugly-number-ii 的多语言解法👆 + +https://leetcode.cn/problems/ugly-number-iii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 二分搜索 + 数学解法 +class Solution { +public: + int nthUglyNumber(int n, int a, int b, int c) { + // 题目说本题结果在 [1, 2 * 10^9] 范围内, + // 所以就按照这个范围初始化两端都闭的搜索区间 + int left = 1, right = (int) 2e9; + // 搜索左侧边界的二分搜索 + while (left <= right) { + int mid = left + (right - left) / 2; + if (f(mid, a, b, c) < n) { + // [1..mid] 中的元素个数不足 n,所以目标在右侧 + left = mid + 1; + } else { + // [1..mid] 中的元素个数大于 n,所以目标在左侧 + right = mid - 1; + } + } + return left; + } + + // 计算 [1..num] 之间有多少个能够被 a 或 b 或 c 整除的数字 + long f(int num, int a, int b, int c) { + long setA = num / a, setB = num / b, setC = num / c; + long setAB = num / lcm(a, b); + long setAC = num / lcm(a, c); + long setBC = num / lcm(b, c); + long setABC = num / lcm(lcm(a, b), c); + // 集合论定理:A + B + C - A ∩ B - A ∩ C - B ∩ C + A ∩ B ∩ C + return setA + setB + setC - setAB - setAC - setBC + setABC; + /* + ![](../pictures/丑数/1.jpg) + */ + } + + // 计算最大公因数(辗转相除/欧几里得算法) + long gcd(long a, long b) { + if (a < b) { + // 保证 a > b + return gcd(b, a); + } + if (b == 0) { + return a; + } + return gcd(b, a % b); + } + + // 最小公倍数 + long lcm(long a, long b) { + // 最小公倍数就是乘积除以最大公因数 + return a * b / gcd(a, b); + } +}; + +// 用合并单链表的思路(超时) +class Solution2 { +public: + int nthUglyNumber(int n, int a, int b, int c) { + // 可以理解为三个有序链表的头结点的值 + long productA = a, productB = b, productC = c; + // 可以理解为合并之后的有序链表上的指针 + int p = 1; + + long min = -666; + + // 开始合并三个有序链表,获取第 n 个节点的值 + while (p <= n) { + // 取三个链表的最小结点 + min = std::min({productA, productB, productC}); + p++; + // 前进最小结点对应链表的指针 + if (min == productA) { + productA += a; + } + if (min == productB) { + productB += b; + } + if (min == productC) { + productC += c; + } + } + return (int) min; + } +}; +``` + +```go +// by chatGPT (go) +// 二分搜索 + 数学解法 +func nthUglyNumber(n int, a int, b int, c int) int { + // 题目说本题结果在 [1, 2 * 10^9] 范围内, + // 所以就按照这个范围初始化两端都闭的搜索区间 + left, right := 1, 2*int(1e9) + // 搜索左侧边界的二分搜索 + for left <= right { + mid := left + (right-left)/2 + if f(mid, a, b, c) < n { + // [1..mid] 中的元素个数不足 n,所以目标在右侧 + left = mid + 1 + } else { + // [1..mid] 中的元素个数大于 n,所以目标在左侧 + right = mid - 1 + } + } + return left +} + +// 计算 [1..num] 之间有多少个能够被 a 或 b 或 c 整除的数字 +func f(num int, a int, b int, c int) int { + setA := num / a + setB := num / b + setC := num / c + setAB := num / lcm(a, b) + setAC := num / lcm(a, c) + setBC := num / lcm(b, c) + setABC := num / lcm(lcm(a, b), c) + // 集合论定理:A + B + C - A ∩ B - A ∩ C - B ∩ C + A ∩ B ∩ C + return setA + setB + setC - setAB - setAC - setBC + setABC +} + +// 计算最大公因数(辗转相除/欧几里得算法) +func gcd(a int64, b int64) int64 { + if a < b { + // 保证 a > b + return gcd(b, a) + } + if b == 0 { + return a + } + return gcd(b, a%b) +} + +// 最小公倍数 +func lcm(a int64, b int64) int64 { + // 最小公倍数就是乘积除以最大公因数 + return a * b / gcd(a, b) +} +``` + +```java +// by labuladong (java) +// 二分搜索 + 数学解法 +class Solution { + public int nthUglyNumber(int n, int a, int b, int c) { + // 题目说本题结果在 [1, 2 * 10^9] 范围内, + // 所以就按照这个范围初始化两端都闭的搜索区间 + int left = 1, right = (int) 2e9; + // 搜索左侧边界的二分搜索 + while (left <= right) { + int mid = left + (right - left) / 2; + if (f(mid, a, b, c) < n) { + // [1..mid] 中的元素个数不足 n,所以目标在右侧 + left = mid + 1; + } else { + // [1..mid] 中的元素个数大于 n,所以目标在左侧 + right = mid - 1; + } + } + return left; + } + + // 计算 [1..num] 之间有多少个能够被 a 或 b 或 c 整除的数字 + long f(int num, int a, int b, int c) { + long setA = num / a, setB = num / b, setC = num / c; + long setAB = num / lcm(a, b); + long setAC = num / lcm(a, c); + long setBC = num / lcm(b, c); + long setABC = num / lcm(lcm(a, b), c); + // 集合论定理:A + B + C - A ∩ B - A ∩ C - B ∩ C + A ∩ B ∩ C + return setA + setB + setC - setAB - setAC - setBC + setABC; + /** + ![](../pictures/丑数/1.jpg) + */ + } + + // 计算最大公因数(辗转相除/欧几里得算法) + long gcd(long a, long b) { + if (a < b) { + // 保证 a > b + return gcd(b, a); + } + if (b == 0) { + return a; + } + return gcd(b, a % b); + } + + // 最小公倍数 + long lcm(long a, long b) { + // 最小公倍数就是乘积除以最大公因数 + return a * b / gcd(a, b); + } +} + +// 用合并单链表的思路(超时) +class Solution2 { + public int nthUglyNumber(int n, int a, int b, int c) { + // 可以理解为三个有序链表的头结点的值 + long productA = a, productB = b, productC = c; + // 可以理解为合并之后的有序链表上的指针 + int p = 1; + + long min = -666; + + // 开始合并三个有序链表,获取第 n 个节点的值 + while (p <= n) { + // 取三个链表的最小结点 + min = Math.min(Math.min(productA, productB), productC); + p++; + // 前进最小结点对应链表的指针 + if (min == productA) { + productA += a; + } + if (min == productB) { + productB += b; + } + if (min == productC) { + productC += c; + } + } + return (int) min; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number} n + * @param {number} a + * @param {number} b + * @param {number} c + * @return {number} + */ +var nthUglyNumber = function(n, a, b, c) { + // 题目说本题结果在 [1, 2 * 10^9] 范围内, + // 所以就按照这个范围初始化两端都闭的搜索区间 + let left = 1, right = 2e9; + // 搜索左侧边界的二分搜索 + while (left <= right) { + let mid = left + (right - left) / 2; + if (f(mid, a, b, c) < n) { + // [1..mid] 中的元素个数不足 n,所以目标在右侧 + left = mid + 1; + } else { + // [1..mid] 中的元素个数大于 n,所以目标在左侧 + right = mid - 1; + } + } + return left; + + // 计算 [1..num] 之间有多少个能够被 a 或 b 或 c 整除的数字 + function f(num, a, b, c) { + let setA = Math.floor(num / a), setB = Math.floor(num / b), setC = Math.floor(num / c); + let setAB = Math.floor(num / lcm(a, b)); + let setAC = Math.floor(num / lcm(a, c)); + let setBC = Math.floor(num / lcm(b, c)); + let setABC = Math.floor(num / lcm(lcm(a, b), c)); + // 集合论定理:A + B + C - A ∩ B - A ∩ C - B ∩ C + A ∩ B ∩ C + return setA + setB + setC - setAB - setAC - setBC + setABC; + /** + ![](../pictures/丑数/1.jpg) + */ + } + + // 计算最大公因数(辗转相除/欧几里得算法) + function gcd(a, b) { + if (a < b) { + // 保证 a > b + return gcd(b, a); + } + if (b === 0) { + return a; + } + return gcd(b, a % b); + } + + // 最小公倍数 + function lcm(a, b) { + // 最小公倍数就是乘积除以最大公因数 + return a * b / gcd(a, b); + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def nthUglyNumber(self, n: int, a: int, b: int, c: int) -> int: + def gcd(x: int, y: int) -> int: + if x < y: + # 保证 x > y + return gcd(y, x) + if y == 0: + return x + return gcd(y, x % y) + + def lcm(x: int, y: int) -> int: + # 最小公倍数就是乘积除以最大公因数 + return x * y // gcd(x, y) + + # 题目说本题结果在 [1, 2 * 10^9] 范围内, + # 所以就按照这个范围初始化两端都闭的搜索区间 + left, right = 1, 2 * 10 ** 9 + # 搜索左侧边界的二分搜索 + while left <= right: + mid = (left + right) // 2 + if (mid // a + mid // b + mid // c - + mid // lcm(a, b) - mid // lcm(b, c) - + mid // lcm(a, c) + mid // lcm(lcm(a, b), c)) < n: + # [1..mid] 中的元素个数不足 n,所以目标在右侧 + left = mid + 1 + else: + # [1..mid] 中的元素个数大于 n,所以目标在左侧 + right = mid - 1 + return left +``` + +https://leetcode.cn/problems/ugly-number-iii 的多语言解法👆 + +https://leetcode.cn/problems/unique-binary-search-trees 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 备忘录 + int memo[20][20]; + + int numTrees(int n) { + // 备忘录的值初始化为 0 + memset(memo, 0, sizeof(memo)); + return count(1, n); + } + + int count(int lo, int hi) { + if (lo > hi) return 1; + // 查备忘录 + if (memo[lo][hi] != 0) { + return memo[lo][hi]; + } + + int res = 0; + for (int mid = lo; mid <= hi; mid++) { + int left = count(lo, mid - 1); + int right = count(mid + 1, hi); + res += left * right; + } + // 将结果存入备忘录 + memo[lo][hi] = res; + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func numTrees(n int) int { + // 备忘录的值初始化为 0 + memo := make([][]int, n+1) + for i := 0; i <= n; i++ { + memo[i] = make([]int, n+1) + } + + var count func(lo, hi int) int + count = func(lo, hi int) int { + if lo > hi { + return 1 + } + // 查备忘录 + if memo[lo][hi] != 0 { + return memo[lo][hi] + } + + res := 0 + for mid := lo; mid <= hi; mid++ { + left := count(lo, mid-1) + right := count(mid+1, hi) + res += left * right + } + // 将结果存入备忘录 + memo[lo][hi] = res + + return res + } + + return count(1, n) +} +``` + +```java +// by labuladong (java) +class Solution { + // 备忘录 + int[][] memo; + + int numTrees(int n) { + // 备忘录的值初始化为 0 + memo = new int[n + 1][n + 1]; + return count(1, n); + } + + int count(int lo, int hi) { + if (lo > hi) return 1; + // 查备忘录 + if (memo[lo][hi] != 0) { + return memo[lo][hi]; + } + + int res = 0; + for (int mid = lo; mid <= hi; mid++) { + int left = count(lo, mid - 1); + int right = count(mid + 1, hi); + res += left * right; + } + // 将结果存入备忘录 + memo[lo][hi] = res; + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var numTrees = function(n) { + //备忘录 + var memo = new Array(n+1).fill(0).map(() => new Array(n+1).fill(0)); + //计算树的个数 + function count(lo, hi) { + //当左端点大于右端点时,返回1 + if (lo > hi) return 1; + //查看备忘录 + if (memo[lo][hi] != 0) { + return memo[lo][hi]; + } + + var res = 0; + //用mid遍历左右子树的所有根节点 + for (var mid = lo; mid <= hi; mid++) { + //计算左子树的个数 + var left = count(lo, mid - 1); + //计算右子树的个数 + var right = count(mid + 1, hi); + //计算总的方案数 + res += left * right; + } + + //将结果存入备忘录 + memo[lo][hi] = res; + + return res; + } + //返回全部区间 [1, n] 的方案数 + return count(1, n); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + # 备忘录 + self.memo = [] + + def numTrees(self, n: int) -> int: + # 备忘录的值初始化为 0 + self.memo = [[0]*(n+1) for _ in range(n+1)] + return self.count(1, n) + + def count(self, lo: int, hi: int) -> int: + if lo > hi: + return 1 + # 查备忘录 + if self.memo[lo][hi] != 0: + return self.memo[lo][hi] + + res = 0 + for mid in range(lo, hi+1): + left = self.count(lo, mid - 1) + right = self.count(mid + 1, hi) + res += left * right + # 将结果存入备忘录 + self.memo[lo][hi] = res + + return res +``` + +https://leetcode.cn/problems/unique-binary-search-trees 的多语言解法👆 + +https://leetcode.cn/problems/unique-binary-search-trees-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + /* 主函数 */ + vector generateTrees(int n) { + if (n == 0) return vector({}); + // 构造闭区间 [1, n] 组成的 BST + return build(1, n); + } + + /* 构造闭区间 [lo, hi] 组成的 BST */ + vector build(int lo, int hi) { + vector res; + // base case + if (lo > hi) { + res.push_back(nullptr); + return res; + } + + // 1、穷举 root 节点的所有可能。 + for (int i = lo; i <= hi; i++) { + // 2、递归构造出左右子树的所有合法 BST。 + vector leftTree = build(lo, i - 1); + vector rightTree = build(i + 1, hi); + // 3、给 root 节点穷举所有左右子树的组合。 + for (auto left : leftTree) { + for (auto right : rightTree) { + // i 作为根节点 root 的值 + TreeNode* root = new TreeNode(i); + root->left = left; + root->right = right; + res.push_back(root); + } + } + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +/* 主函数 */ +func generateTrees(n int) []*TreeNode { + if n == 0 { return []*TreeNode{} } + // 构造闭区间 [1, n] 组成的 BST + return build(1, n) +} + +/* 构造闭区间 [lo, hi] 组成的 BST */ +func build(lo, hi int) []*TreeNode { + res := []*TreeNode{} + // base case + if lo > hi { + res = append(res, nil) + return res + } + + // 1、穷举 root 节点的所有可能。 + for i := lo; i <= hi; i++ { + // 2、递归构造出左右子树的所有合法 BST。 + leftTree := build(lo, i-1) + rightTree := build(i+1, hi) + // 3、给 root 节点穷举所有左右子树的组合。 + for _, left := range leftTree { + for _, right := range rightTree { + // i 作为根节点 root 的值 + root := &TreeNode{Val: i} + root.Left = left + root.Right = right + res = append(res, root) + } + } + } + return res +} +``` + +```java +// by labuladong (java) +class Solution { + /* 主函数 */ + public List generateTrees(int n) { + if (n == 0) return new LinkedList<>(); + // 构造闭区间 [1, n] 组成的 BST + return build(1, n); + } + + /* 构造闭区间 [lo, hi] 组成的 BST */ + List build(int lo, int hi) { + List res = new LinkedList<>(); + // base case + if (lo > hi) { + res.add(null); + return res; + } + + // 1、穷举 root 节点的所有可能。 + for (int i = lo; i <= hi; i++) { + // 2、递归构造出左右子树的所有合法 BST。 + List leftTree = build(lo, i - 1); + List rightTree = build(i + 1, hi); + // 3、给 root 节点穷举所有左右子树的组合。 + for (TreeNode left : leftTree) { + for (TreeNode right : rightTree) { + // i 作为根节点 root 的值 + TreeNode root = new TreeNode(i); + root.left = left; + root.right = right; + res.add(root); + } + } + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var generateTrees = function(n) { + if (n == 0) return new Array(); + // 构造闭区间 [1, n] 组成的 BST + return build(1, n); +} + +var build = function(lo, hi) { + // 存储 BST 的结果集 + var res = new Array(); + // base case + if (lo > hi) { + // 注意:这里必须添加 null,否则在下面无法递归左右子树 + res.push(null); + return res; + } + // 1、穷举 root 节点的所有可能 + for (var i = lo; i <= hi; i++) { + // 2、递归构造出左右子树的所有合法 BST + var leftTree = build(lo, i - 1); + var rightTree = build(i + 1, hi); + // 3、给 root 节点穷举所有左右子树的组合 + for (var j = 0; j < leftTree.length; j++) { + for (var k = 0; k < rightTree.length; k++) { + // i 作为根节点 root 的值 + var root = new TreeNode(i); + root.left = leftTree[j]; + root.right = rightTree[k]; + res.push(root); + } + } + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def generateTrees(self, n: int) -> List[TreeNode]: + if n == 0: # 如果节点数量为 0,则返回一个空列表 + return [] + return self.build(1, n) # 构造闭区间 [1, n] 的 BST + + def build(self, lo: int, hi: int) -> List[TreeNode]: + res = [] + # 当 lo > hi 时,不存在节点,将 None 存入结果列表中,并返回 + if lo > hi: + res.append(None) + return res + + # 1、穷举 root 节点的所有可能。 + for i in range(lo, hi + 1): + # 2、递归构造出左右子树的所有合法 BST。 + leftTree = self.build(lo, i - 1) + rightTree = self.build(i + 1, hi) + # 3、给 root 节点穷举所有左右子树的组合。 + for left in leftTree: + for right in rightTree: + # i 作为根节点 root 的值 + root = TreeNode(i) + root.left = left + root.right = right + res.append(root) + + return res +``` + +https://leetcode.cn/problems/unique-binary-search-trees-ii 的多语言解法👆 + +https://leetcode.cn/problems/unique-paths 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + // 备忘录 + int[][] memo; + +public: + int uniquePaths(int m, int n) { + memo = new int*[m]; + for(int i=0;i 0) { + return memo[x][y]; + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x - 1, y) + dp(x, y - 1); + return memo[x][y]; + } +}; +``` + +```go +// by chatGPT (go) +func uniquePaths(m int, n int) int { + // 备忘录 + memo := make([][]int, m) + for i := 0; i < m; i++ { + memo[i] = make([]int, n) + } + + // 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 + var dp func(int, int) int + dp = func(x int, y int) int { + // base case + if x == 0 && y == 0 { + return 1 + } + if x < 0 || y < 0 { + return 0 + } + // 避免冗余计算 + if memo[x][y] > 0 { + return memo[x][y] + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x-1, y) + dp(x, y-1) + return memo[x][y] + } + + return dp(m-1, n-1) +} +``` + +```java +// by labuladong (java) +class Solution { + // 备忘录 + int[][] memo; + + public int uniquePaths(int m, int n) { + memo = new int[m][n]; + return dp(m - 1, n - 1); + } + + // 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 + int dp(int x, int y) { + // base case + if (x == 0 && y == 0) { + return 1; + } + if (x < 0 || y < 0) { + return 0; + } + // 避免冗余计算 + if (memo[x][y] > 0) { + return memo[x][y]; + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x - 1, y) + dp(x, y - 1); + return memo[x][y]; + } +} +``` + +```javascript +// by chatGPT (javascript) +var uniquePaths = function(m, n) { + // 备忘录 + const memo = new Array(m).fill(0).map(() => new Array(n).fill(0)); + return dp(m - 1, n - 1); + + // 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 + function dp(x, y) { + // base case + if (x === 0 && y === 0) { + return 1; + } + if (x < 0 || y < 0) { + return 0; + } + // 避免冗余计算 + if (memo[x][y] > 0) { + return memo[x][y]; + } + // 状态转移方程: + // 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + memo[x][y] = dp(x - 1, y) + dp(x, y - 1); + return memo[x][y]; + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + # 备忘录 + memo = [] + + def uniquePaths(self, m: int, n: int) -> int: + self.memo = [[0] * n for _ in range(m)] + return self.dp(m - 1, n - 1) + + # 定义:从 (0, 0) 到 (x, y) 有 dp(x, y) 条路径 + def dp(self, x: int, y: int) -> int: + # base case + if x == 0 and y == 0: + return 1 + if x < 0 or y < 0: + return 0 + # 避免冗余计算 + if self.memo[x][y] > 0: + return self.memo[x][y] + # 状态转移方程: + # 到达 (x, y) 的路径数等于到达 (x - 1, y) 和 (x, y - 1) 路径数之和 + self.memo[x][y] = self.dp(x - 1, y) + self.dp(x, y - 1) + return self.memo[x][y] +``` + +https://leetcode.cn/problems/unique-paths 的多语言解法👆 + +https://leetcode.cn/problems/univalued-binary-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isUnivalTree(TreeNode* root) { + if (root == nullptr) { + return true; + } + prev = root->val; + traverse(root); + return isUnival; + } + + int prev; + bool isUnival = true; + + void traverse(TreeNode* root) { + if (root == nullptr || !isUnival) { + return; + } + if (root->val != prev) { + isUnival = false; + return; + } + traverse(root->left); + traverse(root->right); + } +}; +``` + +```go +// by chatGPT (go) +func isUnivalTree(root *TreeNode) bool { + var ( + prev int // Closure to maintain previous node's value + isUnival bool = true // Closure to maintain result, whether tree is unival or not + ) + + var traverse func(root *TreeNode) + traverse = func(root *TreeNode) { + if root == nil || !isUnival { + return + } + if root.Val != prev { + isUnival = false + return + } + traverse(root.Left) + traverse(root.Right) + } + + if root == nil { + return true + } + prev = root.Val + traverse(root) + return isUnival +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isUnivalTree(TreeNode root) { + if (root == null) { + return true; + } + prev = root.val; + traverse(root); + return isUnival; + } + + int prev; + boolean isUnival = true; + + void traverse(TreeNode root) { + if (root == null || !isUnival) { + return; + } + if (root.val != prev) { + isUnival = false; + return; + } + traverse(root.left); + traverse(root.right); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {boolean} + */ +var isUnivalTree = function(root) { + if (root == null) { + return true; + } + + var prev = root.val; + var isUnival = true; + + var traverse = function(root) { + if (root == null || !isUnival) { + return; + } + if (root.val != prev) { + isUnival = false; + return; + } + traverse(root.left); + traverse(root.right); + } + + traverse(root); + return isUnival; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isUnivalTree(self, root: TreeNode) -> bool: + if not root: + return True + self.prev = root.val + self.isUnival = True + self.traverse(root) + return self.isUnival + + def traverse(self, root: TreeNode) -> None: + if not root or not self.isUnival: + return + if root.val != self.prev: + self.isUnival = False + return + self.traverse(root.left) + self.traverse(root.right) +``` + +https://leetcode.cn/problems/univalued-binary-tree 的多语言解法👆 + +https://leetcode.cn/problems/valid-anagram 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 判断字符串 s 和 t 是否为字母异位词 + bool isAnagram(string s, string t) { + // 使用 encode 函数计算两个字符串中各字符出现的次数 + int[] count1 = encode(s); + int[] count2 = encode(t); + // 判断两个字符串中所有字符出现的数量是否相同 + for (int i = 0; i < count1.length; i++) { + if (count1[i] != count2[i]) { + return false; + } + } + return true; + } + + // 计算字符串中各字符出现的次数 + int[] encode(string s) { + int[] count = new int[26]; + for (char c : s.toCharArray()) { + int delta = c - 'a'; + count[delta]++; + } + return count; + } +}; +``` + +```go +// by chatGPT (go) +func isAnagram(s string, t string) bool { + count1 := encode(s) + count2 := encode(t) + // 确保两个字符串中所有字符出现的数量相同 + for i := 0; i < len(count1); i++ { + if count1[i] != count2[i] { + return false + } + } + return true +} + +// 计算字符的出现次数 +func encode(s string) []int { + count := make([]int, 26) + for _, c := range s { + delta := int(c - 'a') + count[delta]++ + } + return count +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isAnagram(String s, String t) { + int[] count1 = encode(s); + int[] count2 = encode(t); + // 确保两个字符串中所有字符出现的数量相同 + for (int i = 0; i < count1.length; i++) { + if (count1[i] != count2[i]) { + return false; + } + } + + return true; + } + + // 计算字符的出现次数 + int[] encode(String s) { + int[] count = new int[26]; + for (char c : s.toCharArray()) { + int delta = c - 'a'; + count[delta]++; + } + return count; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isAnagram = function(s, t) { + var count1 = encode(s); + var count2 = encode(t); + // 确保两个字符串中所有字符出现的数量相同 + for (var i = 0; i < count1.length; i++) { + if (count1[i] !== count2[i]) { + return false; + } + } + + return true; +}; + +// 计算字符的出现次数 +var encode = function(s) { + var count = new Array(26).fill(0); + for (var i = 0; i < s.length; i++) { + var delta = s.charCodeAt(i) - 'a'.charCodeAt(0); + count[delta]++; + } + return count; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + count1 = self.encode(s) + count2 = self.encode(t) + # 确保两个字符串中所有字符出现的数量相同 + for i in range(len(count1)): + if count1[i] != count2[i]: + return False + + return True + + # 计算字符的出现次数 + def encode(self, s: str) -> List[int]: + count = [0] * 26 + for c in s: + delta = ord(c) - ord('a') + count[delta] += 1 + return count +``` + +https://leetcode.cn/problems/valid-anagram 的多语言解法👆 + +https://leetcode.cn/problems/valid-palindrome 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isPalindrome(string s) { + // 先把所有字符转化成小写,并过滤掉空格和标点这类字符 + string nstr; + for(int i=0;i bool: + # 先把所有字符转化成小写,并过滤掉空格和标点这类字符 + sb = [] + for c in s: + if c.isalnum(): + sb.append(c.lower()) + + # 然后对剩下的这些目标字符执行双指针算法,判断回文串 + s = "".join(sb) + # 一左一右两个指针相向而行 + left, right = 0, len(s) - 1 + while left < right: + if s[left] != s[right]: + return False + left += 1 + right -= 1 + return True +``` + +https://leetcode.cn/problems/valid-palindrome 的多语言解法👆 + +https://leetcode.cn/problems/valid-parentheses 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isValid(string str) { + stack left; + for (char c : str) { + if (c == '(' || c == '{' || c == '[') + left.push(c); + else // 字符 c 是右括号 + if (!left.empty() && leftOf(c) == left.top()) + left.pop(); + else + // 和最近的左括号不匹配 + return false; + } + // 是否所有的左括号都被匹配了 + return left.empty(); + } + + char leftOf(char c) { + if (c == '}') return '{'; + if (c == ')') return '('; + return '['; + } +}; +``` + +```go +// by chatGPT (go) +// 包名为 main +// 定义一个字节数组栈类型 +type Stack []byte + +// 入栈操作 +func (s *Stack) push(str byte) { + *s = append(*s, str) +} + +// 出栈操作 +func (s *Stack) pop() byte { + if len(*s) == 0 { + return 0 + } + res := (*s)[len(*s)-1] + *s = (*s)[:len(*s)-1] + return res +} + +// 判断给定字符串是否是合法的括号序列 +func isValid(str string) bool { + // 定义一个栈 left 保存左括号 + var left Stack + // 遍历字符 + for i := range str { + c := str[i] + // 当 c 是左括号时,入栈 left + if c == '(' || c == '[' || c == '{' { + left.push(c) + } else { // 当 c 是右括号时 + // 如果栈 left 非空,且栈顶的左括号和当前右括号匹配,则弹出栈顶元素 + if len(left) != 0 && leftOf(c) == left.pop() { + continue + } else { // 当前左括号和最近的左括号不匹配 + return false + } + } + } + // 是否所有的左括号都被匹配了 + return len(left) == 0 +} + +// 返回左括号 +func leftOf(c byte) byte { + if c == '}' { + return '{' + } else if c == ')' { + return '(' + } else { + return '[' + } +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isValid(String str) { + Stack left = new Stack<>(); + for (char c : str.toCharArray()) { + if (c == '(' || c == '{' || c == '[') + left.push(c); + else // 字符 c 是右括号 + if (!left.isEmpty() && leftOf(c) == left.peek()) + left.pop(); + else + // 和最近的左括号不匹配 + return false; + } + // 是否所有的左括号都被匹配了 + return left.isEmpty(); + } + + char leftOf(char c) { + if (c == '}') return '{'; + if (c == ')') return '('; + return '['; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isValid = function(str) { + // 建立一个栈 + let left = []; + // 遍历字符串中的每一个字符 + for (let c of str) { + // 如果是左括号,则入栈 + if (c == '(' || c == '{' || c == '[') + left.push(c); + else { // 字符 c 是右括号 + // 如果栈不为空,并且最近入栈的左括号可以匹配,则出栈 + if (left.length && leftOf(c) == left[left.length-1]) + left.pop(); + else + // 和最近的左括号不匹配 + return false; + } + } + // 是否所有的左括号都被匹配了 + return !left.length; +} + +function leftOf(c) { + if (c == '}') return '{'; + if (c == ')') return '('; + return '['; +} +``` + +```python +# by chatGPT (python) +class Solution: + def isValid(self, s: str) -> bool: + left = [] # 使用栈结构,存储所有待匹配的左括号 + for c in s: + if c == '(' or c == '{' or c == '[': + left.append(c) # 如果字符 c 是左括号,则将其加入左括号栈 left 中 + else: + if left and self.leftOf(c) == left[-1]: # 如果字符 c 是右括号,则比较它与最近一次加入栈 left 中的左括号是否匹配 + left.pop() # 如果匹配,则将最近的左括号出栈,否则返回 False + else: + return False + return not left # 最后判断栈是否为空,如果是则说明所有的左括号都被匹配了,返回 True,否则返回 False + + def leftOf(self, c: str) -> str: + if c == '}': + return '{' + elif c == ')': + return '(' + else: + return '[' +``` + +https://leetcode.cn/problems/valid-parentheses 的多语言解法👆 + +https://leetcode.cn/problems/validate-binary-search-tree 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isValidBST(TreeNode* root) { + // 限定以root为根的子树节点必须满足max.val > root.val > min.val + return checkValidBST(root, nullptr, nullptr); + } + + // 限定root节点符合min和max的限制 + bool checkValidBST(TreeNode* root, TreeNode* minNode, TreeNode* maxNode) { + // base case + if (root == nullptr) return true; + // 若root.val不符合max和min的限制,说明不是合法BST + if (minNode != nullptr && root->val <= minNode->val) return false; + if (maxNode != nullptr && root->val >= maxNode->val) return false; + // 限定左子树的最大值是root.val,右子树的最小值是root.val + return checkValidBST(root->left, minNode, root) + && checkValidBST(root->right, root, maxNode); + } +}; +``` + +```go +// by chatGPT (go) +func isValidBST(root *TreeNode) bool { + return isValid(root, nil, nil) +} + +/* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */ +func isValid(root *TreeNode, min *TreeNode, max *TreeNode) bool { + // base case + if root == nil { + return true + } + // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST + if min != nil && root.Val <= min.Val { + return false + } + if max != nil && root.Val >= max.Val { + return false + } + // 限定左子树的最大值是 root.Val,右子树的最小值是 root.Val + return isValid(root.Left, min, root) && isValid(root.Right, root, max) +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isValidBST(TreeNode root) { + return isValidBST(root, null, null); + } + + /* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */ + boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) { + // base case + if (root == null) return true; + // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST + if (min != null && root.val <= min.val) return false; + if (max != null && root.val >= max.val) return false; + // 限定左子树的最大值是 root.val,右子树的最小值是 root.val + return isValidBST(root.left, min, root) + && isValidBST(root.right, root, max); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {boolean} + */ +var isValidBST = function(root) { + return helper(root, null, null); +}; + +/** + * 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val + * @param {TreeNode} root + * @param {TreeNode} min + * @param {TreeNode} max + * @return {boolean} + */ +var helper = function(root, min, max) { + // base case + if (root == null) return true; + // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST + if (min != null && root.val <= min.val) return false; + if (max != null && root.val >= max.val) return false; + // 限定左子树的最大值是root.val,右子树的最小值是root.val + return helper(root.left, min, root) && helper(root.right, root, max); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isValidBST(self, root: TreeNode) -> bool: + return self.helper(root, None, None) + + # 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val + def helper(self, root, min_node, max_node): + # base case + if not root: + return True + # 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST + if min_node and root.val <= min_node.val: + return False + if max_node and root.val >= max_node.val: + return False + # 限定左子树的最大值是 root.val,右子树的最小值是 root.val + return self.helper(root.left, min_node, root) and self.helper(root.right, root, max_node) +``` + +https://leetcode.cn/problems/validate-binary-search-tree 的多语言解法👆 + +https://leetcode.cn/problems/video-stitching 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int videoStitching(vector>& clips, int T) { + if (T == 0) return 0; + //按起点升序排列,起点相同的降序排列 + //PS:其实起点相同的不用降序排列也可以,不过我觉得这样更清晰 + sort(clips.begin(), clips.end(), [](vector& a, vector& b){ + if (a[0] == b[0]) { + return b[1] < a[1]; + } + return a[0] < b[0]; + }); + //记录选择的短视频个数 + int res = 0; + + int curEnd = 0, nextEnd = 0; + int i = 0, n = clips.size(); + while (i < n && clips[i][0] <= curEnd) { + //在第res个视频的区间内贪心选择下一个视频 + while (i < n && clips[i][0] <= curEnd) { + nextEnd = max(nextEnd, clips[i][1]); + i++; + } + //找到下一个视频,更新curEnd + res++; + curEnd = nextEnd; + if (curEnd >= T) { + //已经可以拼出区间 [0, T] + return res; + } + } + //无法连续拼出区间 [0, T] + return -1; + } +}; +``` + +```go +// by chatGPT (go) +import "sort" + +func videoStitching(clips [][]int, T int) int { + if T == 0 { + return 0 + } + // 按起点升序排列,起点相同的降序排列 + // PS:其实起点相同的不用降序排列也可以,不过我觉得这样更清晰 + sort.Slice(clips, func(i, j int) bool { + a, b := clips[i], clips[j] + if a[0] == b[0] { + return b[1] < a[1] + } + return a[0] < b[0] + }) + // 记录选择的短视频个数 + var res int + + curEnd, nextEnd := 0, 0 + i, n := 0, len(clips) + for i < n && clips[i][0] <= curEnd { + // 在第 res 个视频的区间内贪心选择下一个视频 + for i < n && clips[i][0] <= curEnd { + nextEnd = max(nextEnd, clips[i][1]) + i++ + } + // 找到下一个视频,更新 curEnd + res++ + curEnd = nextEnd + if curEnd >= T { + // 已经可以拼出区间 [0, T] + return res + } + } + // 无法连续拼出区间 [0, T] + return -1 +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```java +// by labuladong (java) +class Solution { + public int videoStitching(int[][] clips, int T) { + if (T == 0) return 0; + // 按起点升序排列,起点相同的降序排列 + // PS:其实起点相同的不用降序排列也可以,不过我觉得这样更清晰 + Arrays.sort(clips, (a, b) -> { + if (a[0] == b[0]) { + return b[1] - a[1]; + } + return a[0] - b[0]; + }); + // 记录选择的短视频个数 + int res = 0; + + int curEnd = 0, nextEnd = 0; + int i = 0, n = clips.length; + while (i < n && clips[i][0] <= curEnd) { + // 在第 res 个视频的区间内贪心选择下一个视频 + while (i < n && clips[i][0] <= curEnd) { + nextEnd = Math.max(nextEnd, clips[i][1]); + i++; + } + // 找到下一个视频,更新 curEnd + res++; + curEnd = nextEnd; + if (curEnd >= T) { + // 已经可以拼出区间 [0, T] + return res; + } + } + // 无法连续拼出区间 [0, T] + return -1; + } +} +``` + +```javascript +// by chatGPT (javascript) +var videoStitching = function(clips, T) { + if (T == 0) return 0; + // 按起点升序排列,起点相同的降序排列 + // PS:其实起点相同的不用降序排列也可以,不过我觉得这样更清晰 + clips.sort((a, b) => { + if (a[0] == b[0]) { + return b[1] - a[1]; + } + return a[0] - b[0]; + }); + // 记录选择的短视频个数 + var res = 0; + + var curEnd = 0, nextEnd = 0; + var i = 0, n = clips.length; + while (i < n && clips[i][0] <= curEnd) { + // 在第 res 个视频的区间内贪心选择下一个视频 + while (i < n && clips[i][0] <= curEnd) { + nextEnd = Math.max(nextEnd, clips[i][1]); + i++; + } + // 找到下一个视频,更新 curEnd + res++; + curEnd = nextEnd; + if (curEnd >= T) { + // 已经可以拼出区间 [0, T] + return res; + } + } + // 无法连续拼出区间 [0, T] + return -1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def videoStitching(self, clips: List[List[int]], T: int) -> int: + if T == 0: + return 0 + # 按起点升序排列,起点相同的降序排列 + # PS:其实起点相同的不用降序排列也可以,不过我觉得这样更清晰 + clips.sort(key=lambda x: (x[0], -x[1])) + # 记录选择的短视频个数 + res = 0 + curEnd, nextEnd = 0, 0 + i, n = 0, len(clips) + while i < n and clips[i][0] <= curEnd: + # 在第 res 个视频的区间内贪心选择下一个视频 + while i < n and clips[i][0] <= curEnd: + nextEnd = max(nextEnd, clips[i][1]) + i += 1 + # 找到下一个视频,更新 curEnd + res += 1 + curEnd = nextEnd + if curEnd >= T: + # 已经可以拼出区间 [0, T] + return res + # 无法连续拼出区间 [0, T] + return -1 +``` + +https://leetcode.cn/problems/video-stitching 的多语言解法👆 + +https://leetcode.cn/problems/vvXgSW 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +// Definition for singly-linked list. +// struct ListNode { +// int val; +// ListNode *next; +// ListNode(int x) : val(x), next(NULL) {} +// }; + +class Solution { +public: + ListNode* mergeKLists(vector& lists) { + if (lists.size() == 0) { return nullptr; } + // 虚拟头结点 + ListNode* dummy = new ListNode(-1); + ListNode* p = dummy; + // 优先级队列,最小堆 + auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; }; + priority_queue, decltype(cmp)> pq(cmp); + + // 将 k 个链表的头结点加入最小堆 + for (auto head : lists) { + if (head != nullptr) + pq.push(head); + } + + while (!pq.empty()) { + // 获取最小节点,接到结果链表中 + ListNode* node = pq.top(); + p->next = node; + pq.pop(); + if (node->next != nullptr) { + pq.push(node->next); + } + // p 指针不断前进 + p = p->next; + } + return dummy->next; + } +}; +``` + +```go +// by chatGPT (go) +func mergeKLists(lists []*ListNode) *ListNode { + if len(lists) == 0 { + return nil + } + + // 虚拟头结点 + dummy := &ListNode{} + p := dummy + + // 优先级队列,最小堆 + pq := make(PriorityQueue, 0) + for _, head := range lists { + if head != nil { + pq.Push(head) + } + } + + for !pq.IsEmpty() { + // 获取最小节点,接到结果链表中 + node := pq.Pop().(*ListNode) + p.Next = node + if node.Next != nil { + pq.Push(node.Next) + } + // p 指针不断前进 + p = p.Next + } + return dummy.Next +} + +// 优先级队列,最小堆 +type PriorityQueue []*ListNode + +func (pq PriorityQueue) Len() int { return len(pq) } + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].Val < pq[j].Val +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + *pq = append(*pq, x.(*ListNode)) +} + +func (pq *PriorityQueue) Pop() interface{} { + n := len(*pq) + x := (*pq)[n-1] + *pq = (*pq)[:n-1] + return x +} + +func (pq PriorityQueue) IsEmpty() bool { + return len(pq) == 0 +} +``` + +```java +// by labuladong (java) +class Solution { + public ListNode mergeKLists(ListNode[] lists) { + if (lists.length == 0) return null; + // 虚拟头结点 + ListNode dummy = new ListNode(-1); + ListNode p = dummy; + // 优先级队列,最小堆 + PriorityQueue pq = new PriorityQueue<>( + lists.length, (a, b)->(a.val - b.val)); + // 将 k 个链表的头结点加入最小堆 + for (ListNode head : lists) { + if (head != null) + pq.add(head); + } + + while (!pq.isEmpty()) { + // 获取最小节点,接到结果链表中 + ListNode node = pq.poll(); + p.next = node; + if (node.next != null) { + pq.add(node.next); + } + // p 指针不断前进 + p = p.next; + } + return dummy.next; + } +} +``` + +```javascript +// by chatGPT (javascript) +var mergeKLists = function(lists) { + if (lists.length == 0) return null; + // 虚拟头结点 + var dummy = new ListNode(-1); + var p = dummy; + // 优先级队列,最小堆 + var pq = new PriorityQueue( + lists.length, (a, b) => (a.val - b.val)); + // 将 k 个链表的头结点加入最小堆 + for (var head of lists) { + if (head != null) + pq.add(head); + } + + while (!pq.isEmpty()) { + // 获取最小节点,接到结果链表中 + var node = pq.poll(); + p.next = node; + if (node.next != null) { + pq.add(node.next); + } + // p 指针不断前进 + p = p.next; + } + return dummy.next; +} +``` + +```python +# by chatGPT (python) +class Solution: + def mergeKLists(self, lists: List[ListNode]) -> ListNode: + if not lists: + return None + # 虚拟头结点 + dummy = ListNode(-1) + p = dummy + # 优先级队列,最小堆 + pq = [] + for head in lists: + if head: + # 将 k 个链表的头结点加入最小堆 + heapq.heappush(pq, (head.val, head)) + while pq: + # 获取最小节点,接到结果链表中 + val, node = heapq.heappop(pq) + p.next = node + if node.next: + heapq.heappush(pq, (node.next.val, node.next)) + # p 指针不断前进 + p = p.next + return dummy.next +``` + +https://leetcode.cn/problems/vvXgSW 的多语言解法👆 + +https://leetcode.cn/problems/w6cpku 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 记录累加和 + int sum = 0; + TreeNode* convertBST(TreeNode* root) { + traverse(root); + return root; + } + + // 遍历右子树 -> 维护累加和 -> 转换为累加树 -> 遍历左子树 + void traverse(TreeNode* root) { + if (root == nullptr) { + return; + } + traverse(root->right); // 遍历右子树 + sum += root->val; // 维护累加和 + root->val = sum; // 将BST转换为累加树 + traverse(root->left); // 遍历左子树 + } +}; +``` + +```go +// by chatGPT (go) +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +func convertBST(root *TreeNode) *TreeNode { + var sum int + var traverse func(*TreeNode) + //记录累加和 + traverse = func(root *TreeNode) { + if root == nil { + return + } + traverse(root.Right) + //维护累加和 + sum += root.Val + //将BST转化成累加树 + root.Val = sum + traverse(root.Left) + } + traverse(root) + return root +} +``` + +```java +// by labuladong (java) +class Solution { + public TreeNode convertBST(TreeNode root) { + traverse(root); + return root; + } + + // 记录累加和 + int sum = 0; + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.right); + // 维护累加和 + sum += root.val; + // 将 BST 转化成累加树 + root.val = sum; + traverse(root.left); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {TreeNode} root + * @return {TreeNode} + */ +var convertBST = function(root) { + // 记录累加和 + let sum = 0; + function traverse(root) { + if (root === null) { + return; + } + traverse(root.right); + // 维护累加和 + sum += root.val; + // 将 BST 转化成累加树 + root.val = sum; + traverse(root.left); + } + + traverse(root); + return root; +} +``` + +```python +# by chatGPT (python) +class Solution: + def convertBST(self, root: TreeNode) -> TreeNode: + self.sum = 0 #记录累加和 + self.traverse(root) + return root + + def traverse(self, root: TreeNode) -> None: + if not root: + return + self.traverse(root.right) + self.sum += root.val #维护累加和 + root.val = self.sum #将 BST 转化成累加树 + self.traverse(root.left) +``` + +https://leetcode.cn/problems/w6cpku 的多语言解法👆 + +https://leetcode.cn/problems/walls-and-gates 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + void wallsAndGates(vector>& rooms) { + const int ROOM = INT_MAX; + const int GATE = 0; + + int m = rooms.size(); + int n = rooms[0].size(); + + queue> q; + // 把所有的门放入队列,作为 BFS 遍历的起点 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (rooms[i][j] == GATE) { + q.push({i, j}); + } + } + } + + vector> dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + // 开始执行 BFS 算法,根据 BFS 算法的特性,第一次遍历到新节点所走的步数就是最短距离 + while (!q.empty()) { + auto cur = q.front(); + q.pop(); + int x = cur.first, y = cur.second; + for (auto d : dirs) { + int next_x = x + d[0], next_y = y + d[1]; + if (next_x < 0 || next_y < 0 || next_x >= m || next_y >= n + || rooms[next_x][next_y] != ROOM) { + // 这个位置是障碍物或者已经计算出最短距离了,跳过 + continue; + } + // 计算出 (next_x, next_y) 达到最近的门的距离 + rooms[next_x][next_y] = rooms[x][y] + 1; + q.push({next_x, next_y}); + } + } + } +}; +``` + +```go +// by chatGPT (go) +func wallsAndGates(rooms [][]int) { + const ROOM, GATE = math.MaxInt32, 0 + + m, n := len(rooms), len(rooms[0]) + + q := [][]int{} + // 把所有的门放入队列,作为 BFS 遍历的起点 + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if rooms[i][j] == GATE { + q = append(q, []int{i, j}) + } + } + } + + dirs := [][]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} + // 开始执行 BFS 算法,根据 BFS 算法的特性,第一次遍历到新节点所走的步数就是最短距离 + for len(q) > 0 { + cur := q[0] + q = q[1:] + x, y := cur[0], cur[1] + for _, d := range dirs { + next_x, next_y := x+d[0], y+d[1] + if next_x < 0 || next_y < 0 || next_x >= m || next_y >= n || rooms[next_x][next_y] != ROOM { + // 这个位置是障碍物或者已经计算出最短距离了,跳过 + continue + } + // 计算出 (next_x, next_y) 达到最近的门的距离 + rooms[next_x][next_y] = rooms[x][y] + 1 + q = append(q, []int{next_x, next_y}) + } + } +} +``` + +```java +// by labuladong (java) +class Solution { + + public void wallsAndGates(int[][] rooms) { + final int ROOM = Integer.MAX_VALUE; + final int GATE = 0; + + int m = rooms.length; + int n = rooms[0].length; + + Queue q = new LinkedList<>(); + // 把所有的门放入队列,作为 BFS 遍历的起点 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (rooms[i][j] == GATE) { + q.add(new int[]{i, j}); + } + } + } + + int[][] dirs = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + // 开始执行 BFS 算法,根据 BFS 算法的特性,第一次遍历到新节点所走的步数就是最短距离 + while (!q.isEmpty()) { + int[] cur = q.poll(); + int x = cur[0], y = cur[1]; + for (int[] d : dirs) { + int next_x = x + d[0], next_y = y + d[1]; + if (next_x < 0 || next_y < 0 || next_x >= m || next_y >= n + || rooms[next_x][next_y] != ROOM) { + // 这个位置是障碍物或者已经计算出最短距离了,跳过 + continue; + } + // 计算出 (next_x, next_y) 达到最近的门的距离 + rooms[next_x][next_y] = rooms[x][y] + 1; + q.offer(new int[]{next_x, next_y}); + } + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[][]} rooms + * @return {void} Do not return anything, modify rooms in-place instead. + */ + +var wallsAndGates = function(rooms) { + const ROOM = Infinity; + const GATE = 0; + + let m = rooms.length; + let n = rooms[0].length; + + let q = []; + // 把所有的门放入队列,作为 BFS 遍历的起点 + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (rooms[i][j] === GATE) { + q.push([i, j]); + } + } + } + + let dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + // 开始执行 BFS 算法,根据 BFS 算法的特性,第一次遍历到新节点所走的步数就是最短距离 + while (q.length) { + let cur = q.shift(); + let x = cur[0], y = cur[1]; + + for (let i = 0; i < dirs.length; i++) { + let next_x = x + dirs[i][0], next_y = y + dirs[i][1]; + if (next_x < 0 || next_y < 0 || next_x >= m || next_y >= n || rooms[next_x][next_y] !== ROOM) { + // 这个位置是障碍物或者已经计算出最短距离了,跳过 + continue; + } + // 计算出 (next_x, next_y) 达到最近的门的距离 + rooms[next_x][next_y] = rooms[x][y] + 1; + q.push([next_x, next_y]); + } + } +}; +``` + +```python +# by chatGPT (python) +from collections import deque + +class Solution: + def wallsAndGates(self, rooms: List[List[int]]) -> None: + """ + Do not return anything, modify rooms in-place instead. + """ + ROOM = float('inf') + GATE = 0 + + m = len(rooms) + n = len(rooms[0]) + + q = deque() + # 把所有的门放入队列,作为 BFS 遍历的起点 + for i in range(m): + for j in range(n): + if rooms[i][j] == GATE: + q.append([i, j]) + + dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]] + # 开始执行 BFS 算法,根据 BFS 算法的特性,第一次遍历到新节点所走的步数就是最短距离 + while q: + cur = q.popleft() + x = cur[0] + y = cur[1] + for d in dirs: + next_x = x + d[0] + next_y = y + d[1] + if next_x < 0 or next_y < 0 or next_x >= m or next_y >= n or rooms[next_x][next_y] != ROOM: + # 这个位置是障碍物或者已经计算出最短距离了,跳过 + continue + # 计算出 (next_x, next_y) 达到最近的门的距离 + rooms[next_x][next_y] = rooms[x][y] + 1 + q.append([next_x, next_y]) +``` + +https://leetcode.cn/problems/walls-and-gates 的多语言解法👆 + +https://leetcode.cn/problems/wildcard-matching 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + // 备忘录,-1 代表还未计算,0 代表 false,1 代表 true + vector> memo; + + bool isMatch(string s, string p) { + if (p.empty()) { + return s.empty(); + } + // 将 p 中相邻的 * 去除,以提升效率 + string pp = remove_adj_star(p); + int m = s.size(), n = pp.size(); + // 备忘录初始化为 -1 + memo = vector>(m, vector(n, -1)); + // 执行自顶向下带备忘录的动态规划 + return dp(s, 0, pp, 0); + } + + // 删除相邻的 * 号,返回删除后的字符 + string remove_adj_star(string p) { + if (p.empty()) { + return ""; + } + string pp; + pp.push_back(p[0]); + for (int i = 1; i < p.size(); i++) { + if (p[i] == '*' && p[i - 1] == '*') { + continue; + } + pp.push_back(p[i]); + } + return pp; + } + + // 定义:判断 s[i..] 是否能被 p[j..] 匹配 + bool dp(string& s, int i, string& p, int j) { + // base case + if (j == p.size() && i == s.size()) { + return true; + } + if (i == s.size()) { + for (int k = j; k < p.size(); k++) { + if (p[k] != '*') { + return false; + } + } + return true; + } + if (j == p.size()) { + return false; + } + if (memo[i][j] != -1) { + return bool(memo[i][j]); + } + + bool res = false; + if (s[i] == p[j] || p[j] == '?') { + // s[i] 和 p[j] 完成匹配 + res = dp(s, i + 1, p, j + 1); + } else if (p[j] == '*') { + // s[i] 和 p[j] 不匹配,但 p[j] 是通配符 * + // 可以匹配 0 个或多个 s 中的字符, + // 只要有一种情况能够完成匹配即可 + res = dp(s, i + 1, p, j) + || dp(s, i, p, j + 1); + } + // 将 s[i] 和 p[j] 的匹配结果存储备忘录 + memo[i][j] = res; + + return res; + } +}; +``` + +```go +// by chatGPT (go) +func isMatch(s string, p string) bool { + // 备忘录,-1 代表还未计算,0 代表 false,1 代表 true + memo := make([][]int, len(s)) + for i := range memo { + memo[i] = make([]int, len(p)) + for j := range memo[i] { + memo[i][j] = -1 + } + } + + if len(p) == 0 { + return len(s) == 0 + } + // 将 p 中相邻的 * 去除,以提升效率 + pp := removeAdjStar(p) + + return dp(s, 0, pp, 0, memo) +} + +// 删除相邻的 * 号,返回删除后的字符 +func removeAdjStar(p string) string { + if len(p) == 0 { + return "" + } + pp := []byte{p[0]} + for i := 1; i < len(p); i++ { + if p[i] == '*' && p[i-1] == '*' { + continue + } + pp = append(pp, p[i]) + } + return string(pp) +} + +// 定义:判断 s[i..] 是否能被 p[j..] 匹配 +func dp(s string, i int, p string, j int, memo [][]int) bool { + // base case + if j == len(p) && i == len(s) { + return true + } + if i == len(s) { + for k := j; k < len(p); k++ { + if p[k] != '*' { + return false + } + } + return true + } + if j == len(p) { + return false + } + if memo[i][j] != -1 { + return memo[i][j] == 1 + } + + var res bool + if s[i] == p[j] || p[j] == '?' { + // s[i] 和 p[j] 完成匹配 + res = dp(s, i+1, p, j+1, memo) + } else if p[j] == '*' { + // s[i] 和 p[j] 不匹配,但 p[j] 是通配符 * + // 可以匹配 0 个或多个 s 中的字符, + // 只要有一种情况能够完成匹配即可 + res = dp(s, i+1, p, j, memo) || dp(s, i, p, j+1, memo) + } + // 将 s[i] 和 p[j] 的匹配结果存储备忘录 + memo[i][j] = 0 + if res { + memo[i][j] = 1 + } + + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + // 备忘录,-1 代表还未计算,0 代表 false,1 代表 true + private int[][] memo; + + public boolean isMatch(String s, String p) { + if (p.isEmpty()) { + return s.isEmpty(); + } + // 将 p 中相邻的 * 去除,以提升效率 + String pp = removeAdjStar(p); + int m = s.length(), n = pp.length(); + // 备忘录初始化为 -1 + memo = new int[m][n]; + for (int i = 0; i < m; i++) { + Arrays.fill(memo[i], -1); + } + // 执行自顶向下带备忘录的动态规划 + return dp(s, 0, pp, 0); + } + + // 删除相邻的 * 号,返回删除后的字符 + private String removeAdjStar(String p) { + if (p.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(p.charAt(0)); + for (int i = 1; i < p.length(); i++) { + if (p.charAt(i) == '*' && p.charAt(i - 1) == '*') { + continue; + } + sb.append(p.charAt(i)); + } + return sb.toString(); + } + + // 定义:判断 s[i..] 是否能被 p[j..] 匹配 + private boolean dp(String s, int i, String p, int j) { + // base case + if (j == p.length() && i == s.length()) { + return true; + } + if (i == s.length()) { + for (int k = j; k < p.length(); k++) { + if (p.charAt(k) != '*') { + return false; + } + } + return true; + } + if (j == p.length()) { + return false; + } + if (memo[i][j] != -1) { + return memo[i][j] == 1; + } + + boolean res = false; + if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '?') { + // s[i] 和 p[j] 完成匹配 + res = dp(s, i + 1, p, j + 1); + } else if (p.charAt(j) == '*') { + // s[i] 和 p[j] 不匹配,但 p[j] 是通配符 * + // 可以匹配 0 个或多个 s 中的字符, + // 只要有一种情况能够完成匹配即可 + res = dp(s, i + 1, p, j) + || dp(s, i, p, j + 1); + } + // 将 s[i] 和 p[j] 的匹配结果存储备忘录 + memo[i][j] = res ? 1 : 0; + + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isMatch = function(s, p) { + // 备忘录,-1 代表还未计算,0 代表 false,1 代表 true + var memo = []; + + function remove_adj_star(p) { + if (p.length === 0) { + return ""; + } + var pp = p[0]; + for (var i = 1; i < p.length; i++) { + if (p[i] === '*' && p[i - 1] == '*') { + continue; + } + pp += p[i]; + } + return pp; + } + + // 定义:判断 s[i..] 是否能被 p[j..] 匹配 + function dp(s, i, p, j) { + // base case + if (j === p.length && i === s.length) { + return true; + } + if (i === s.length) { + for (var k = j; k < p.length; k++) { + if (p[k] !== '*') { + return false; + } + } + return true; + } + if (j === p.length) { + return false; + } + if (typeof memo[i] !== "undefined" && typeof memo[i][j] !== "undefined" && memo[i][j] !== -1) { + return memo[i][j] === 1; + } + + var res = false; + if (s[i] === p[j] || p[j] === '?') { + // s[i] 和 p[j] 完成匹配 + res = dp(s, i + 1, p, j + 1); + } else if (p[j] === '*') { + // s[i] 和 p[j] 不匹配,但 p[j] 是通配符 * + // 可以匹配 0 个或多个 s 中的字符, + // 只要有一种情况能够完成匹配即可 + res = dp(s, i + 1, p, j) || dp(s, i, p, j + 1); + } + // 将 s[i] 和 p[j] 的匹配结果存储备忘录 + if (typeof memo[i] === "undefined") { + memo[i] = []; + } + memo[i][j] = res ? 1 : 0; + + return res; + } + + // 将 p 中相邻的 * 去除,以提升效率 + var pp = remove_adj_star(p); + var m = s.length, n = pp.length; + // 备忘录初始化为 -1 + for (var i = 0; i < m; i++) { + memo[i] = []; + for (var j = 0; j < n; j++) { + memo[i][j] = -1; + } + } + // 执行自顶向下带备忘录的动态规划 + return dp(s, 0, pp, 0); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isMatch(self, s: str, p: str) -> bool: + if not p: + return not s + # 将 p 中相邻的 * 去除,以提升效率 + pp = self.remove_adj_star(p) + m, n = len(s), len(pp) + # 备忘录初始化为 -1 + memo = [[-1] * n for _ in range(m)] + # 执行自顶向下带备忘录的动态规划 + return self.dp(s, 0, pp, 0, memo) + + # 删除相邻的 * 号,返回删除后的字符 + def remove_adj_star(self, p: str) -> str: + if not p: + return "" + pp = p[0] + for i in range(1, len(p)): + if p[i] == '*' and p[i - 1] == '*': + continue + pp += p[i] + return pp + + # 定义:判断 s[i..] 是否能被 p[j..] 匹配 + def dp(self, s: str, i: int, p: str, j: int, memo: List[List[int]]) -> bool: + # base case + if j == len(p) and i == len(s): + return True + if i == len(s): + for k in range(j, len(p)): + if p[k] != '*': + return False + return True + if j == len(p): + return False + if memo[i][j] != -1: + return bool(memo[i][j]) + + res = False + if s[i] == p[j] or p[j] == '?': + # s[i] 和 p[j] 完成匹配 + res = self.dp(s, i + 1, p, j + 1, memo) + elif p[j] == '*': + # s[i] 和 p[j] 不匹配,但 p[j] 是通配符 * + # 可以匹配 0 个或多个 s 中的字符, + # 只要有一种情况能够完成匹配即可 + res = self.dp(s, i + 1, p, j, memo) or self.dp(s, i, p, j + 1, memo) + # 将 s[i] 和 p[j] 的匹配结果存储备忘录 + memo[i][j] = res + + return res +``` + +https://leetcode.cn/problems/wildcard-matching 的多语言解法👆 + +https://leetcode.cn/problems/word-break-ii 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector res; + // 记录回溯路径 + deque track; + + vector wordBreak(string s, vector& wordDict) { + // 根据函数定义,判断 s[0..] 是否能够被拼出 + backtrack(s, 0, wordDict); + return res; + } + + // 回溯算法框架 + void backtrack(string s, int i, vector& wordDict) { + // base case,整个 s 都被拼出来了 + if (i == s.length()) { + res.emplace_back(track[0]); + for (int j = 1; j < track.size(); ++j) { + res.back() += " " + track[j]; + } + return; + } + if (i > s.length()) { + return; + } + + // 遍历所有单词,尝试匹配 s[i..] 的前缀 + for (const string& word : wordDict) { + int len = word.length(); + // 单词太长,跳过 + if (i + len > s.length()) { + continue; + } + // 无法匹配,跳过 + string subStr = s.substr(i, len); + if (subStr != word) { + continue; + } + // s[i..] 的前缀被 word 匹配,做选择 + track.emplace_back(word); + backtrack(s, i + len, wordDict); + // 撤销选择 + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +func wordBreak(s string, wordDict []string) []string { + res := []string{} + // 记录回溯路径 + track := []string{} + // 根据函数定义,判断 s[0..] 是否能够被拼出 + backtrack(s, 0, wordDict, &track, &res) + return res +} + +// 回溯算法框架 +func backtrack(s string, i int, wordDict []string, track *[]string, res *[]string) { + // base case,整个 s 都被拼出来了 + if i == len(s) { + *res = append(*res, strings.Join(*track, " ")) + return + } + if i > len(s) { + return + } + + // 遍历所有单词,尝试匹配 s[i..] 的前缀 + for _, word := range(wordDict) { + len := len(word) + // 单词太长,跳过 + if i + len > len(s) { + continue + } + // 无法匹配,跳过 + subStr := s[i:i+len] + if subStr != word { + continue + } + // s[i..] 的前缀被 word 匹配,做选择 + *track = append(*track, word) + backtrack(s, i + len, wordDict, track, res) + // 撤销选择 + *track = (*track)[:len(*track)-1] + } +} +``` + +```java +// by labuladong (java) +class Solution { + + List res = new LinkedList<>(); + // 记录回溯路径 + LinkedList track = new LinkedList<>(); + + public List wordBreak(String s, List wordDict) { + // 根据函数定义,判断 s[0..] 是否能够被拼出 + backtrack(s, 0, wordDict); + return res; + } + + // 回溯算法框架 + void backtrack(String s, int i, List wordDict) { + // base case,整个 s 都被拼出来了 + if (i == s.length()) { + res.add(String.join(" ", track)); + return; + } + if (i > s.length()) { + return; + } + + // 遍历所有单词,尝试匹配 s[i..] 的前缀 + for (String word : wordDict) { + int len = word.length(); + // 单词太长,跳过 + if (i + len > s.length()) { + continue; + } + // 无法匹配,跳过 + String subStr = s.substring(i, i + len); + if (!subStr.equals(word)) { + continue; + } + // s[i..] 的前缀被 word 匹配,做选择 + track.addLast(word); + backtrack(s, i + len, wordDict); + // 撤销选择 + track.removeLast(); + } + } +} +``` + +```javascript +// by chatGPT (javascript) +var wordBreak = function(s, wordDict) { + let res = []; // 记录结果 + let track = []; // 记录回溯路径 + + // 回溯算法框架 + function backtrack(s, i, wordDict) { + // base case,整个 s 都被拼出来了 + if (i === s.length) { + res.push(track.join(" ")); + return; + } + if (i > s.length) { + return; + } + + // 遍历所有单词,尝试匹配 s[i..] 的前缀 + for (let word of wordDict) { + let len = word.length; + // 单词太长,跳过 + if (i + len > s.length) { + continue; + } + // 无法匹配,跳过 + let subStr = s.substring(i, i + len); + if (subStr !== word) { + continue; + } + // s[i..] 的前缀被 word 匹配,做选择 + track.push(word); + backtrack(s, i + len, wordDict); + // 撤销选择 + track.pop(); + } + } + + backtrack(s, 0, wordDict); + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.res = [] + # 记录回溯路径 + self.track = [] + + def wordBreak(self, s: str, wordDict: List[str]) -> List[str]: + # 根据函数定义,判断 s[0..] 是否能够被拼出 + self.backtrack(s, 0, wordDict) + return self.res + + # 回溯算法框架 + def backtrack(self, s: str, i: int, wordDict: List[str]) -> None: + # base case,整个 s 都被拼出来了 + if i == len(s): + self.res.append(" ".join(self.track)) + return + if i > len(s): + return + + # 遍历所有单词,尝试匹配 s[i..] 的前缀 + for word in wordDict: + length = len(word) + # 单词太长,跳过 + if i + length > len(s): + continue + # 无法匹配,跳过 + sub_str = s[i:i + length] + if sub_str != word: + continue + # s[i..] 的前缀被 word 匹配,做选择 + self.track.append(word) + self.backtrack(s, i + length, wordDict) + # 撤销选择 + self.track.pop() +``` + +https://leetcode.cn/problems/word-break-ii 的多语言解法👆 + +https://leetcode.cn/problems/word-pattern 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool wordPattern(string pattern, string s) { + vector words; + string sub_s = ""; + + //将单词存入vector中 + for (int i = 0; i < s.size(); i++) { + if (s[i] == ' ') { + words.push_back(sub_s); + sub_s = ""; + } else { + sub_s += s[i]; + } + } + words.push_back(sub_s); + if (pattern.size() != words.size()) { + return false; + } + + //记录pattern字符到单词的映射 + unordered_map patternToWord; + //记录哪些已经有pattern对应的单词 + unordered_set wordSet; + + for (int i = 0; i < pattern.size(); i++) { + char c = pattern[i]; + string word = words[i]; + if (!patternToWord.count(c)) { + if (wordSet.count(word)) { + //这个单词以前已经有其他模式字符对应了 + return false; + } + //添加c->word的映射 + patternToWord[c] = word; + } else { + //这个pattern字符已经出现过,确保和之前对应的单词相同 + if (patternToWord[c] != word) { + return false; + } + } + //这个单词已经有模式字符对应 + wordSet.insert(word); + + } + return true; + } +}; + +class Solution2 { +public: + bool wordPattern(string pattern, string s) { + vector words; + string sub_s = ""; + + //将单词存入vector中 + for (int i = 0; i < s.size(); i++) { + if (s[i] == ' ') { + words.push_back(sub_s); + sub_s = ""; + } else { + sub_s += s[i]; + } + } + words.push_back(sub_s); + if (pattern.size() != words.size()) { + return false; + } + + //记录pattern字符到单词的映射 + unordered_map wordToPattern; + unordered_set patternCharSet; + + for (int i = 0; i < pattern.size(); i++) { + char c = pattern[i]; + string word = words[i]; + if (!wordToPattern.count(word)) { + //当前这个单词还没有对应的模式字符 + if (patternCharSet.count(c)) { + //对应的模式字符之前已经对应了其他单词 + return false; + } + //添加word->c的映射 + wordToPattern[word] = c; + } + else { + //这个单词之前已经出现过,确保当前单词和之前对应的模式字符相同 + if (wordToPattern[word] != c) { + return false; + } + } + patternCharSet.insert(c); + } + return true; + } +}; +``` + +```go +// by chatGPT (go) +func wordPattern(pattern string, s string) bool { + words := strings.Split(s, " ") + if len(pattern) != len(words) { + return false + } + // 记录 pattern 字符到单词的映射 + patternToWord := make(map[byte]string) + // 记录那些已经有 pattern 对应的单词 + wordSet := make(map[string]bool) + + for i := 0; i < len(pattern); i++ { + c := pattern[i] + word := words[i] + if _, ok := patternToWord[c]; !ok { + if _, ok := wordSet[word]; ok { + // 这个单词以前已经有其他模式字符对应了 + return false + } + // 添加 c -> word 的映射 + patternToWord[c] = word + } else { + // 这个 pattern 字符已经出现过,确保和之前对应的单词相同 + if patternToWord[c] != word { + return false + } + } + // 这个单词已经有模式字符对应 + wordSet[word] = true + } + return true +} + + +func wordPattern2(pattern string, s string) bool { + words := strings.Split(s, " ") + if len(pattern) != len(words) { + return false + } + // 记录 word 到 pattern 字符的映射 + wordToPattern := make(map[string]byte) + // 记录那些已经有单词对应的 pattern 字符 + patternCharSet := make(map[byte]bool) + + for i := 0; i < len(pattern); i++ { + c := pattern[i] + word := words[i] + if _, ok := wordToPattern[word]; !ok { + // 当前这个单词还没有对应的模式字符 + if _, ok := patternCharSet[c]; ok { + // 对应的模式字符之前已经对应了其他单词 + return false + } + // 添加 word -> c 的映射 + wordToPattern[word] = c + } else { + // 这个单词之前已经出现过,确保当前单词和之前对应的模式字符相同 + if wordToPattern[word] != c { + return false + } + } + patternCharSet[c] = true + } + return true +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean wordPattern(String pattern, String s) { + String[] words = s.split(" "); + if (pattern.length() != words.length) { + return false; + } + // 记录 pattern 字符到单词的映射 + HashMap patternToWord = new HashMap<>(); + // 记录那些已经有 pattern 对应的单词 + HashSet wordSet = new HashSet<>(); + + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + String word = words[i]; + if (!patternToWord.containsKey(c)) { + if (wordSet.contains(word)) { + // 这个单词以前已经有其他模式字符对应了 + return false; + } + // 添加 c -> word 的映射 + patternToWord.put(c, word); + } else { + // 这个 pattern 字符已经出现过,确保和之前对应的单词相同 + if (!patternToWord.get(c).equals(word)) { + return false; + } + } + // 这个单词已经有模式字符对应 + wordSet.add(word); + } + return true; + } +} + + + +class Solution2 { + public boolean wordPattern(String pattern, String s) { + String[] words = s.split(" "); + if (pattern.length() != words.length) { + return false; + } + // 记录 pattern 字符到单词的映射 + HashMap wordToPattern = new HashMap<>(); + HashSet patternCharSet = new HashSet<>(); + + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + String word = words[i]; + if (!wordToPattern.containsKey(word)) { + // 当前这个单词还没有对应的模式字符 + if (patternCharSet.contains(c)) { + // 对应的模式字符之前已经对应了其他单词 + return false; + } + // 添加 word -> c 的映射 + wordToPattern.put(word, c); + } else { + // 这个单词之前已经出现过,确保当前单词和之前对应的模式字符相同 + if (!wordToPattern.get(word).equals(c)) { + return false; + } + } + patternCharSet.add(c); + } + return true; + } +} +``` + +```javascript +// by chatGPT (javascript) +var wordPattern = function(pattern, s) { + var words = s.split(" "); + if (pattern.length !== words.length) { + return false; + } + // 记录 pattern 字符到单词的映射 + var patternToWord = new Map(); + // 记录那些已经有 pattern 对应的单词 + var wordSet = new Set(); + + for (var i = 0; i < pattern.length; i++) { + var c = pattern.charAt(i); + var word = words[i]; + if (!patternToWord.has(c)) { + if (wordSet.has(word)) { + // 这个单词以前已经有其他模式字符对应了 + return false; + } + // 添加 c -> word 的映射 + patternToWord.set(c, word); + } else { + // 这个 pattern 字符已经出现过,确保和之前对应的单词相同 + if (!patternToWord.get(c) === (word)) { + return false; + } + } + // 这个单词已经有模式字符对应 + wordSet.add(word); + } + return true; +}; + + +var wordPattern2 = function(pattern, s) { + var words = s.split(" "); + if (pattern.length !== words.length) { + return false; + } + // 记录 pattern 字符到单词的映射 + var wordToPattern = new Map(); + var patternCharSet = new Set(); + + for (var i = 0; i < pattern.length; i++) { + var c = pattern.charAt(i); + var word = words[i]; + if (!wordToPattern.has(word)) { + // 当前这个单词还没有对应的模式字符 + if (patternCharSet.has(c)) { + // 对应的模式字符之前已经对应了其他单词 + return false; + } + // 添加 word -> c 的映射 + wordToPattern.set(word, c); + } else { + // 这个单词之前已经出现过,确保当前单词和之前对应的模式字符相同 + if (!wordToPattern.get(word) === c) { + return false; + } + } + patternCharSet.add(c); + } + return true; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def wordPattern(self, pattern: str, s: str) -> bool: + words = s.split(" ") + if len(pattern) != len(words): + return False + #记录 pattern 字符到单词的映射 + patternToWord = {} + #记录那些已经有 pattern 对应的单词 + wordSet = set() + + for i in range(len(pattern)): + c = pattern[i] + word = words[i] + if c not in patternToWord: + if word in wordSet: + #这个单词以前已经有其他模式字符对应了 + return False + #添加 c -> word 的映射 + patternToWord[c] = word + else: + #这个 pattern 字符已经出现过,确保和之前对应的单词相同 + if patternToWord[c] != word: + return False + #这个单词已经有模式字符对应 + wordSet.add(word) + return True + + +class Solution2: + def wordPattern(self, pattern: str, s: str) -> bool: + words = s.split(" ") + if len(pattern) != len(words): + return False + #记录 pattern 字符到单词的映射 + wordToPattern = {} + patternCharSet = set() + + for i in range(len(pattern)): + c = pattern[i] + word = words[i] + if word not in wordToPattern: + #当前这个单词还没有对应的模式字符 + if c in patternCharSet: + #对应的模式字符之前已经对应了其他单词 + return False + #添加 word -> c 的映射 + wordToPattern[word] = c + else: + #这个单词之前已经出现过,确保当前单词和之前对应的模式字符相同 + if wordToPattern[word] != c: + return False + patternCharSet.add(c) + return True +``` + +https://leetcode.cn/problems/word-pattern 的多语言解法👆 + +https://leetcode.cn/problems/word-search 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { + bool found = false; +public: + bool exist(vector>& board, string word) { + int m = board.size(), n = board[0].size(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + dfs(board, i, j, word, 0); + if (found) { // 如果找到了一个答案,直接返回true + return true; + } + } + } + return false; // 没有找到答案,返回false + } + + // 从 (i, j) 开始向四周搜索,试图匹配 word[p..] + void dfs(vector>& board, int i, int j, string& word, int p) { + if (p == word.size()) { // 整个word已经被匹配完,找到了一个答案 + found = true; + return; + } + if (found) { // 已经找到了一个答案,不用再搜索了 + return; + } + int m = board.size(), n = board[0].size(); + if (i < 0 || j < 0 || i >= m || j >= n) { // 在边界之外,返回 + return; + } + if (board[i][j] != word[p]) { // 与当前字符不匹配,返回 + return; + } + + // 因为之前匹配的字符不能重复匹配,所以我们给它们添加一个负号标记 + board[i][j] = (char)(-board[i][j]); + dfs(board, i + 1, j, word, p + 1); // 向下搜索 + dfs(board, i, j + 1, word, p + 1); // 向右搜索 + dfs(board, i - 1, j, word, p + 1); // 向上搜索 + dfs(board, i, j - 1, word, p + 1); // 向左搜索 + board[i][j] = (char)(-board[i][j]); // 回溯当前字符 + } +}; +``` + +```go +// by chatGPT (go) +func exist(board [][]byte, word string) bool { + m, n := len(board), len(board[0]) + found := false + var dfs func(i, j, p int) + dfs = func(i, j, p int) { + if p == len(word) { + // 整个 word 已经被匹配完,找到了一个答案 + found = true + return + } + if found { + // 已经找到了一个答案,不用再搜索了 + return + } + if i < 0 || j < 0 || i >= m || j >= n { + return + } + if board[i][j] != word[p] { + return + } + + // 已经匹配过的字符,我们给它添一个负号作为标记,避免走回头路 + board[i][j] ^= 255 + // word[p] 被 board[i][j] 匹配,开始向四周搜索 word[p+1..] + dfs(i+1, j, p+1) + dfs(i, j+1, p+1) + dfs(i-1, j, p+1) + dfs(i, j-1, p+1) + board[i][j] ^= 255 + } + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + dfs(i, j, 0) + if found { + return true + } + } + } + return false +} +``` + +```java +// by labuladong (java) +class Solution { + + boolean found = false; + + public boolean exist(char[][] board, String word) { + int m = board.length, n = board[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + dfs(board, i, j, word, 0); + if (found) { + return true; + } + } + } + return false; + } + + // 从 (i, j) 开始向四周搜索,试图匹配 word[p..] + void dfs(char[][] board, int i, int j, String word, int p) { + if (p == word.length()) { + // 整个 word 已经被匹配完,找到了一个答案 + found = true; + return; + } + if (found) { + // 已经找到了一个答案,不用再搜索了 + return; + } + int m = board.length, n = board[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (board[i][j] != word.charAt(p)) { + return; + } + + // 已经匹配过的字符,我们给它添一个负号作为标记,避免走回头路 + board[i][j] = (char)(-board[i][j]); + // word[p] 被 board[i][j] 匹配,开始向四周搜索 word[p+1..] + dfs(board, i + 1, j, word, p + 1); + dfs(board, i, j + 1, word, p + 1); + dfs(board, i - 1, j, word, p + 1); + dfs(board, i, j - 1, word, p + 1); + board[i][j] = (char)(-board[i][j]); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {character[][]} board + * @param {string} word + * @return {boolean} + */ +var exist = function(board, word) { + let found = false; + let dfs = function(board, i, j, word, p){ + // 当整个 word 已经被匹配完,找到了一个答案 + if (p == word.length) { + found = true; + return; + } + // 已经找到了一个答案,不用再搜索了 + if (found) { + return; + } + let m = board.length, n = board[0].length; + // 如果越界了,直接返回 + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + // 如果当前字符和 word 的当前字符不相等,返回 + if (board[i][j] != word.charAt(p)) { + return; + } + // 已经匹配过的字符,我们给它添一个负号作为标记,避免走回头路 + board[i][j] = -board[i][j]; + // word[p] 被 board[i][j] 匹配,开始向四周搜索 word[p+1..] + dfs(board, i + 1, j, word, p + 1); + dfs(board, i, j + 1, word, p + 1); + dfs(board, i - 1, j, word, p + 1); + dfs(board, i, j - 1, word, p + 1); + board[i][j] = -board[i][j]; + } + let m = board.length, n = board[0].length; + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + dfs(board, i, j, word, 0); + if (found) { + return true; + } + } + } + return false; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.found = False + + def exist(self, board: List[List[str]], word: str) -> bool: + m, n = len(board), len(board[0]) + for i in range(m): + for j in range(n): + self.dfs(board, i, j, word, 0) + # 如果找到了一个答案,则直接返回True + if self.found: + return True + return False + + # 从 (i, j) 开始向四周搜索,试图匹配 word[p..] + def dfs(self, board, i, j, word, p): + if p == len(word): + # 整个 word 已经被匹配完,找到了一个答案 + self.found = True + return + if self.found: + # 已经找到了一个答案,不用再搜索了 + return + m, n = len(board), len(board[0]) + if i < 0 or j < 0 or i >= m or j >= n: + return + if board[i][j] != word[p]: + return + + # 已经匹配过的字符,我们给它添一个负号作为标记,避免走回头路 + board[i][j] = -board[i][j] + + # word[p] 被 board[i][j] 匹配,开始向四周搜索 word[p+1..] + self.dfs(board, i+1, j, word, p+1) + self.dfs(board, i, j+1, word, p+1) + self.dfs(board, i-1, j, word, p+1) + self.dfs(board, i, j-1, word, p+1) + + # 还原标记(恢复现场) + board[i][j] = -board[i][j] +``` + +https://leetcode.cn/problems/word-search 的多语言解法👆 + +https://leetcode.cn/problems/wtcaE1 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + int lengthOfLongestSubstring(string s) { + unordered_map window; + + int left = 0, right = 0; + int res = 0; // 记录结果 + while (right < s.size()) { + char c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + window[c]++; + // 判断左侧窗口是否要收缩 + while (window[c] > 1) { + char d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + window[d]--; + } + // 在这里更新答案 + res = max(res, right - left); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +func lengthOfLongestSubstring(s string) int { + window := make(map[byte]int) // 用哈希表记录窗口中每个字符出现的次数 + left, right := 0, 0 // 定义一些变量,记录窗口的左右边界和最长子串的长度 + res := 0 + for right < len(s) { // 当右边界没有到字符串的末尾时,执行循环 + c := s[right] // 获取当前右边界指向的字符 + right++ // 右边界后移一位 + window[c]++ // 哈希表中加入当前字符,记录出现的次数 + for window[c] > 1 { // 如果当前字符在窗口中的出现次数大于1,则进入循环 + d := s[left] // 获取窗口中最左边的字符 + left++ // 左边界右移一位 + window[d]-- // 从哈希表中移除最左边的字符 + } + res = max(res, right - left) // 更新最长子串的长度 + } + return res // 返回最长子串的长度 +} + +func max(a, b int) int { // 定义一个函数用于返回两个整数中的最大值 + if a > b { // 如果a大于b + return a // 返回a + } + return b // 否则返回b +} +``` + +```java +// by chatGPT (java) +class Solution { + public int lengthOfLongestSubstring(String s) { + // 使用哈希表记录窗口内字符出现次数 + Map window = new HashMap<>(); + + int left = 0, right = 0; + int res = 0; // 记录结果 + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + window.put(c, window.getOrDefault(c, 0) + 1); + + // 判断左侧窗口是否要收缩 + while (window.get(c) > 1) { + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + window.put(d, window.getOrDefault(d, 0) - 1); + } + // 在这里更新答案 + res = Math.max(res, right - left); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @return {number} + */ +var lengthOfLongestSubstring = function(s) { + // 创建一个映射表,用于记录字母出现的次数 + let window = {}; + // 定义两个指针,分别表示窗口的左右边界 + let left = 0; + let right = 0; + // 定义一个变量,用于记录最长的子串长度 + let res = 0; + + // 当右边界没有超过字符串s的长度时,一直循环 + while (right < s.length) { + let c = s[right]; + right++; + + // 更新映射表 + window[c] = window[c] ? window[c] + 1 : 1; + + // 如果当前字母出现的次数大于1(即窗口内已经有了该字母),需要收缩左边界,使得窗口内不再有重复的字母 + while (window[c] > 1) { + let d = s[left]; + left++; + + // 更新映射表 + window[d]--; + } + + // 更新当前最长的子串长度 + res = Math.max(res, right - left); + } + + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + # 创建一个哈希表 + window = {} + + # 定义左右指针和结果变量 + left, right = 0, 0 + res = 0 + + # 当右指针小于字符串长度时 + while right < len(s): + c = s[right] + right += 1 + + # 将字符添加到哈希表中并更新出现的次数 + if c not in window: + window[c] = 1 + else: + window[c] += 1 + + # 当窗口中字符出现次数大于 1 时,将左指针向右移动,更新哈希表中的值 + while window[c] > 1: + d = s[left] + left += 1 + window[d] -= 1 + + # 更新结果变量,记录最长子串长度 + res = max(res, right - left) + + # 返回最长子串长度 + return res +``` + +https://leetcode.cn/problems/wtcaE1 的多语言解法👆 + +https://leetcode.cn/problems/xu-lie-hua-er-cha-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Codec { +public: + string SEP = ","; + string EMPTY = "#"; + + /* 主函数,将二叉树序列化为字符串 */ + string serialize(TreeNode* root) { + string res = ""; + serialize(root, res); + return res; + } + + /* 辅助函数,将二叉树存入字符串 */ + void serialize(TreeNode* root, string& res) { + if (root == nullptr) { + res += EMPTY + SEP; + return; + } + + /******前序遍历位置******/ + res += to_string(root->val) + SEP; + /***********************/ + + serialize(root->left, res); + serialize(root->right, res); + } + + /* 主函数,将字符串反序列化为二叉树结构 */ + TreeNode* deserialize(string data) { + // 将字符串转化成列表 + vector nodes; + string s = ""; + for (char& c : data) { + if (c == SEP[0]) { + nodes.emplace_back(s); + s = ""; + } + else { + s += c; + } + } + if (!s.empty()) nodes.emplace_back(s); + return deserialize(nodes); + } + + /* 辅助函数,通过 nodes 列表构造二叉树 */ + TreeNode* deserialize(vector& nodes) { + if (nodes.empty()) return nullptr; + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + string first = nodes[0]; + nodes.erase(nodes.begin()); + if (first == EMPTY) return nullptr; + TreeNode* root = new TreeNode(stoi(first)); + /***********************/ + + root->left = deserialize(nodes); + root->right = deserialize(nodes); + + return root; + } +}; +``` + +```go +// by chatGPT (go) +type Codec struct { + SEP string + NULL string +} + +func Constructor() Codec { + return Codec{ + SEP: ",", + NULL: "#", + } +} + +/* 辅助函数,将二叉树存入字符串 */ +func (this *Codec) serialize(root *TreeNode, sb *strings.Builder) { + if root == nil { + sb.WriteString(this.NULL) + sb.WriteString(this.SEP) + return + } + + /******前序遍历位置******/ + sb.WriteString(strconv.Itoa(root.Val)) + sb.WriteString(this.SEP) + /***********************/ + + this.serialize(root.Left, sb) + this.serialize(root.Right, sb) +} + +/* 主函数,将二叉树序列化为字符串 */ +func (this *Codec) serialize(root *TreeNode) string { + var sb strings.Builder + this.serialize(root, &sb) + return sb.String() +} + +/* 辅助函数,根据字符串构建二叉树 */ +func (this *Codec) deserialize(nodes *[]string) *TreeNode { + if len(*nodes) == 0 { + return nil + } + /* 从 nodes 中取出一个元素作为当前子树的根节点 */ + first := (*nodes)[0] + if first == this.NULL { + // 若该元素为 "#" ,代表其为空节点,直接弹出并返回 nil + *nodes = (*nodes)[1:] + return nil + } + /* 否则,该元素实际上是整数,将其转化为节点 */ + val, _ := strconv.Atoi(first) + root := &TreeNode{Val: val} + /* 递归构造左右子树 */ + *nodes = (*nodes)[1:] + root.Left = this.deserialize(nodes) + root.Right = this.deserialize(nodes) + return root +} + +/* 主函数,将字符串反序列化为二叉树结构 */ +func (this *Codec) deserialize(data string) *TreeNode { + nodes := strings.Split(data, this.SEP) + return this.deserialize(&nodes) +} +``` + +```java +// by labuladong (java) +public class Codec { + String SEP = ","; + String NULL = "#"; + + /* 主函数,将二叉树序列化为字符串 */ + public String serialize(TreeNode root) { + StringBuilder sb = new StringBuilder(); + serialize(root, sb); + return sb.toString(); + } + + /* 辅助函数,将二叉树存入 StringBuilder */ + void serialize(TreeNode root, StringBuilder sb) { + if (root == null) { + sb.append(NULL).append(SEP); + return; + } + + /******前序遍历位置******/ + sb.append(root.val).append(SEP); + /***********************/ + + serialize(root.left, sb); + serialize(root.right, sb); + } + + /* 主函数,将字符串反序列化为二叉树结构 */ + public TreeNode deserialize(String data) { + // 将字符串转化成列表 + LinkedList nodes = new LinkedList<>(); + for (String s : data.split(SEP)) { + nodes.addLast(s); + } + return deserialize(nodes); + } + + /* 辅助函数,通过 nodes 列表构造二叉树 */ + TreeNode deserialize(LinkedList nodes) { + if (nodes.isEmpty()) return null; + + /******前序遍历位置******/ + // 列表最左侧就是根节点 + String first = nodes.removeFirst(); + if (first.equals(NULL)) return null; + TreeNode root = new TreeNode(Integer.parseInt(first)); + /***********************/ + + root.left = deserialize(nodes); + root.right = deserialize(nodes); + + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * Encodes a tree to a single string. + * + * @param {TreeNode} root + * @return {string} + */ +var serialize = function(root) { + if(!root) { + return '#,'; // 把空节点转换为 "#," + } + return root.val + ',' + serialize(root.left) + serialize(root.right); // 先把当前节点的值存入字符串,然后递归地序列化左子树和右子树 +}; + +/** + * Decodes your encoded data to tree. + * + * @param {string} data + * @return {TreeNode} + */ +var deserialize = function(data) { + var nodes = data.split(','); // 把字符串转换成节点列表 + var dfs = function() { + var value = nodes.shift(); // 当前字符串对应的节点 + if(value === '#') { // 遇到空节点,返回 null + return null; + } + var node = new TreeNode(value); // 创建当前节点 + node.left = dfs(); // 递归构造左子树 + node.right = dfs(); // 递归构造右子树 + return node; + }; + return dfs(); // 从根节点开始构造二叉树 +}; + +/** + * Your functions will be called as such: + * deserialize(serialize(root)); + */ +``` + +```python +# by chatGPT (python) +class Codec: + def __init__(self): + self.SEP = "," + self.NULL = "#" + + # 主函数,将二叉树序列化为字符串 + def serialize(self, root: TreeNode) -> str: + sb = [] + self._serialize(root, sb) + return ''.join(sb) + + # 辅助函数,将二叉树存入列表 + def _serialize(self, root: TreeNode, sb: list): + if not root: + sb.append(self.NULL).append(self.SEP) + return + + # 前序遍历 + sb.append(str(root.val)).append(self.SEP) + + self._serialize(root.left, sb) + self._serialize(root.right, sb) + + # 主函数,将字符串反序列化为二叉树结构 + def deserialize(self, data: str) -> TreeNode: + # 将字符串转化成列表 + nodes = data.split(self.SEP) + return self._deserialize(nodes) + + # 辅助函数,通过 nodes 列表构造二叉树 + def _deserialize(self, nodes: list) -> TreeNode: + if not nodes: + return None + + # 前序遍历 + first = nodes.pop(0) + if first == self.NULL: + return None + root = TreeNode(int(first)) + + root.left = self._deserialize(nodes) + root.right = self._deserialize(nodes) + + return root +``` + +https://leetcode.cn/problems/xu-lie-hua-er-cha-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/xx4gT2 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int findKthLargest(vector& nums, int k) { + priority_queue, greater> pq; // 小顶堆,堆顶是最小元素 + for (auto e : nums) { + // 每个元素都要过一遍二叉堆 + pq.push(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.pop(); + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return pq.top(); + } +}; +``` + +```go +// by chatGPT (go) +import "container/heap" + +func findKthLargest(nums []int, k int) int { + pq := &minHeap{} + heap.Init(pq) + for _, e := range nums { + // 每个元素都要过一遍小根堆 + heap.Push(pq, e) + // 堆中元素多于 k 个时,删除堆顶元素 + if pq.Len() > k { + heap.Pop(pq) + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return (*pq)[0] +} + +type minHeap []int + +func (h minHeap) Len() int { return len(h) } +func (h minHeap) Less(i, j int) bool { return h[i] < h[j] } +func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h *minHeap) Push(x interface{}) { + *h = append(*h, x.(int)) +} + +func (h *minHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[:n-1] + return x +} +``` + +```java +// by labuladong (java) +class Solution { + public int findKthLargest(int[] nums, int k) { + // 小顶堆,堆顶是最小元素 + PriorityQueue pq = new PriorityQueue<>(); + for (int e : nums) { + // 每个元素都要过一遍二叉堆 + pq.offer(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.poll(); + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return pq.peek(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var findKthLargest = function(nums, k) { + // 小顶堆,堆顶是最小元素 + const pq = new PriorityQueue() + for (let i = 0; i < nums.length; i++) { + const e = nums[i] + // 每个元素都要过一遍二叉堆 + pq.offer(e) + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.poll(); + } + } + // pq 中剩下的是 nums 中 k 个最大元素, + // 堆顶是最小的那个,即第 k 个最大元素 + return pq.peek() +}; +``` + +```python +# by chatGPT (python) +import heapq + +class Solution: + def findKthLargest(self, nums: List[int], k: int) -> int: + # 小顶堆,堆顶是最小元素 + pq = [] + for e in nums: + # 每个元素都要过一遍二叉堆 + heapq.heappush(pq, e) + # 堆中元素多于 k 个时,删除堆顶元素 + if len(pq) > k: + heapq.heappop(pq) + # pq 中剩下的是 nums 中 k 个最大元素, + # 堆顶是最小的那个,即第 k 个最大元素 + return pq[0] +``` + +https://leetcode.cn/problems/xx4gT2 的多语言解法👆 + +https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class MyQueue { +private: + stack s1, s2; +public: + MyQueue() { + s1 = stack(); + s2 = stack(); + } + + /** + * 添加元素到队尾 + */ + void push(int x) { + s1.push(x); + } + + /** + * 删除队头的元素并返回 + */ + int pop() { + // 先调用 peek 保证 s2 非空 + peek(); + int val = s2.top(); + s2.pop(); + return val; + } + + /** + * 返回队头元素 + */ + int peek() { + if (s2.empty()) + // 把 s1 元素压入 s2 + while (!s1.empty()) { + int val = s1.top(); + s1.pop(); + s2.push(val); + } + return s2.top(); + } + + /** + * 判断队列是否为空 + */ + bool empty() { + return s1.empty() && s2.empty(); + } +}; +``` + +```go +// by chatGPT (go) +type MyQueue struct { + s1, s2 *stack.Stack +} + +func Constructor() MyQueue { + return MyQueue{ + s1: stack.New(), + s2: stack.New(), + } +} + +/** + * 添加元素到队尾 + */ +func (this *MyQueue) Push(x int) { + this.s1.Push(x) +} + +/** + * 删除队头的元素并返回 + */ +func (this *MyQueue) Pop() int { + // 先调用 Peek 保证 s2 非空 + this.Peek() + return this.s2.Pop().(int) +} + +/** + * 返回队头元素 + */ +func (this *MyQueue) Peek() int { + if this.s2.Empty() { + // 把 s1 元素压入 s2 + for !this.s1.Empty() { + this.s2.Push(this.s1.Pop()) + } + } + return this.s2.Peek().(int) +} + +/** + * 判断队列是否为空 + */ +func (this *MyQueue) Empty() bool { + return this.s1.Empty() && this.s2.Empty() +} +``` + +```java +// by labuladong (java) +class MyQueue { + private Stack s1, s2; + + public MyQueue() { + s1 = new Stack<>(); + s2 = new Stack<>(); + } + + /** + * 添加元素到队尾 + */ + public void push(int x) { + s1.push(x); + } + + /** + * 删除队头的元素并返回 + */ + public int pop() { + // 先调用 peek 保证 s2 非空 + peek(); + return s2.pop(); + } + + /** + * 返回队头元素 + */ + public int peek() { + if (s2.isEmpty()) + // 把 s1 元素压入 s2 + while (!s1.isEmpty()) + s2.push(s1.pop()); + return s2.peek(); + } + + /** + * 判断队列是否为空 + */ + public boolean empty() { + return s1.isEmpty() && s2.isEmpty(); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * Initialize your data structure here. + */ +var MyQueue = function() { + this.s1 = []; + this.s2 = []; +}; + +/** + * Push element x to the back of queue. + * @param {number} x + * @return {void} + */ +MyQueue.prototype.push = function(x) { + this.s1.push(x); +}; + +/** + * Removes the element from in front of queue and returns that element. + * @return {number} + */ +MyQueue.prototype.pop = function() { + // 先调用 peek 保证 s2 非空 + this.peek(); + return this.s2.pop(); +}; + +/** + * Get the front element. + * @return {number} + */ +MyQueue.prototype.peek = function() { + if (this.s2.length === 0) { + // 把 s1 元素压入 s2 + while (this.s1.length !== 0) { + this.s2.push(this.s1.pop()); + } + } + return this.s2[this.s2.length - 1]; +}; + +/** + * Returns whether the queue is empty. + * @return {boolean} + */ +MyQueue.prototype.empty = function() { + return this.s1.length === 0 && this.s2.length === 0; +}; +``` + +```python +# by chatGPT (python) +class CQueue: + def __init__(self): + """ + 初始化一个队列,使用两个堆栈 s1 和 s2 + """ + self.s1 = [] + self.s2 = [] + + def appendTail(self, value): + """ + 添加元素到队尾 + """ + self.s1.append(value) + + def deleteHead(self): + """ + 删除队头的元素并返回 + """ + # 先调用 peek 保证 s2 非空 + if self.peek() is None: + return -1 + else: + return self.s2.pop() + + def peek(self): + """ + 返回队头元素 + """ + if not self.s2: + # 把 s1 元素压入 s2 + while self.s1: + self.s2.append(self.s1.pop()) + + return self.s2[-1] if self.s2 else None + + + def empty(self): + """ + 判断队列是否为空 + """ + return not self.s1 and not self.s2 +``` + +https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof 的多语言解法👆 + +https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int search(vector& nums, int target) { + int left_index = left_bound(nums, target); + if (left_index == -1) { + return 0; + } + int right_index = right_bound(nums, target); + // 根据左右边界即可推导出元素出现的次数 + return right_index - left_index + 1; + } + + int left_bound(vector& nums, int target) { + int left = 0, right = nums.size() - 1; + // 搜索区间为 [left, right] + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1; + } else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1; + } else if (nums[mid] == target) { + // 收缩右侧边界 + right = mid - 1; + } + } + // 检查出界情况 + if (left >= nums.size() || nums[left] != target) { + /** + ![](../pictures/二分查找/2.jpg) + */ + return -1; + } + return left; + } + + int right_bound(vector& nums, int target) { + int left = 0, right = nums.size() - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // 这里改成收缩左侧边界即可 + left = mid + 1; + } + } + // 这里改为检查 right 越界的情况,见下图 + if (right < 0 || nums[right] != target) { + /** + ![](../pictures/二分查找/4.jpg) + */ + return -1; + } + return right; + } +}; +``` + +```go +// by chatGPT (go) +// 二分查找 +// 在排序数组中查找元素的第一个和最后一个位置 +func search(nums []int, target int) int { + // 元素第一次出现的位置 + leftIndex := leftBound(nums, target) + if leftIndex == -1 { + return 0 + } + // 元素最后一次出现的位置 + rightIndex := rightBound(nums, target) + // 根据左右边界即可推导出元素出现的次数 + return rightIndex - leftIndex + 1 +} + +// 二分查找,查找元素第一次出现的位置 +func leftBound(nums []int, target int) int { + left, right := 0, len(nums)-1 + // 搜索区间为 [left, right] + for left <= right { + mid := left + (right-left)/2 + if nums[mid] < target { + // 搜索区间变为 [mid+1, right] + left = mid + 1 + } else if nums[mid] > target { + // 搜索区间变为 [left, mid-1] + right = mid - 1 + } else if nums[mid] == target { + // 收缩右侧边界 + right = mid - 1 + } + } + // 检查出界情况 + if left >= len(nums) || nums[left] != target { + // 该元素在数组中不存在的情况 + return -1 + } + return left +} + +// 二分查找,查找元素最后一次出现的位置 +func rightBound(nums []int, target int) int { + left, right := 0, len(nums)-1 + // 搜索区间为 [left, right] + for left <= right { + mid := left + (right-left)/2 + if nums[mid] < target { + // 搜索区间变为 [mid+1, right] + left = mid + 1 + } else if nums[mid] > target { + // 搜索区间变为 [left, mid-1] + right = mid - 1 + } else if nums[mid] == target { + // 收缩左侧边界 + left = mid + 1 + } + } + // 检查出界情况 + if right < 0 || nums[right] != target { + // 该元素在数组中不存在的情况 + return -1 + } + return right +} +``` + +```java +// by labuladong (java) +class Solution { + public int search(int[] nums, int target) { + int left_index = left_bound(nums, target); + if (left_index == -1) { + return 0; + } + int right_index = right_bound(nums, target); + // 根据左右边界即可推导出元素出现的次数 + return right_index - left_index + 1; + } + + int left_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + // 搜索区间为 [left, right] + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1; + } else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1; + } else if (nums[mid] == target) { + // 收缩右侧边界 + right = mid - 1; + } + } + // 检查出界情况 + if (left >= nums.length || nums[left] != target) { + /** + ![](../pictures/二分查找/2.jpg) + */ + return -1; + } + return left; + } + + int right_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // 这里改成收缩左侧边界即可 + left = mid + 1; + } + } + // 这里改为检查 right 越界的情况,见下图 + if (right < 0 || nums[right] != target) { + /** + ![](../pictures/二分查找/4.jpg) + */ + return -1; + } + return right; + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {number[]} nums + * @param {number} target + * @return {number} + */ +var search = function(nums, target) { + function left_bound(nums, target) { + let left = 0 + let right = nums.length - 1 + // 搜索区间为 [left, right] + while (left <= right) { + let mid = left + Math.floor((right - left) / 2) + if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1 + } else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1 + } else if (nums[mid] === target) { + // 收缩右侧边界 + right = mid - 1 + } + } + // 检查出界情况 + if (left >= nums.length || nums[left] !== target) { + /** + ![](../pictures/二分查找/2.jpg) + */ + return -1; + } + return left; + } + + function right_bound(nums, target) { + let left = 0 + let right = nums.length - 1 + while (left <= right) { + let mid = left + Math.floor((right - left) / 2) + if (nums[mid] < target) { + left = mid + 1 + } else if (nums[mid] > target) { + right = mid - 1 + } else if (nums[mid] === target) { + // 这里改成收缩左侧边界即可 + left = mid + 1 + } + } + // 这里改为检查 right 越界的情况,见下图 + if (right < 0 || nums[right] !== target) { + /** + ![](../pictures/二分查找/4.jpg) + */ + return -1; + } + return right; + } + + let left_index = left_bound(nums, target); + if (left_index === -1) { + return 0; + } + let right_index = right_bound(nums, target); + // 根据左右边界即可推导出元素出现的次数 + return right_index - left_index + 1; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def search(self, nums: List[int], target: int) -> int: + left_index = self.left_bound(nums, target) + if left_index == -1: + return 0 + right_index = self.right_bound(nums, target) + # 根据左右边界即可推导出元素出现的次数 + return right_index - left_index + 1 + + def left_bound(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) - 1 + # 搜索区间为 [left, right] + while left <= right: + mid = left + (right - left) // 2 + if nums[mid] < target: + # 搜索区间变为 [mid+1, right] + left = mid + 1 + elif nums[mid] > target: + # 搜索区间变为 [left, mid-1] + right = mid - 1 + elif nums[mid] == target: + # 收缩右侧边界 + right = mid - 1 + # 检查出界情况 + if left >= len(nums) or nums[left] != target: + """ + + ![](../pictures/二分查找/2.jpg) + """ + return -1 + return left + + def right_bound(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) - 1 + while left <= right: + mid = left + (right - left) // 2 + if nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid - 1 + elif nums[mid] == target: + # 这里改成收缩左侧边界即可 + left = mid + 1 + # 这里改为检查 right 越界的情况,见下图 + if right < 0 or nums[right] != target: + """ + + ![](../pictures/二分查找/4.jpg) + """ + return -1 + return right +``` + +https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof 的多语言解法👆 + +https://leetcode.cn/problems/zheng-ze-biao-da-shi-pi-pei-lcof 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + // 备忘录 + vector> memo; + + bool isMatch(string s, string p) { + int m = s.size(), n = p.size(); + memo = vector>(m, vector(n, -1)); + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0); + } + + /* 计算 p[j..] 是否匹配 s[i..] */ + bool dp(string& s, int i, string& p, int j) { + int m = s.size(), n = p.size(); + // base case + if (j == n) { + return i == m; + } + if (i == m) { + if ((n - j) % 2 == 1) { + return false; + } + for (; j + 1 < n; j += 2) { + if (p[j + 1] != '*') { + return false; + } + } + return true; + } + + // 查备忘录,防止重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + + bool res = false; + + if (s[i] == p[j] || p[j] == '.') { + if (j < n - 1 && p[j + 1] == '*') { + res = dp(s, i, p, j + 2) + || dp(s, i + 1, p, j); + } else { + res = dp(s, i + 1, p, j + 1); + } + } else { + if (j < n - 1 && p[j + 1] == '*') { + res = dp(s, i, p, j + 2); + } else { + res = false; + } + } + // 将当前结果记入备忘录 + memo[i][j] = res; + return res; + } +}; +``` + +```go +// by chatGPT (go) +func isMatch(s string, p string) bool { + // 备忘录 + memo := make([][]int, len(s)) + for i := range memo { + memo[i] = make([]int, len(p)) + for j := range memo[i] { + memo[i][j] = -1 + } + } + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0, memo) +} + +/* 计算 p[j..] 是否匹配 s[i..] */ +func dp(s string, i int, p string, j int, memo [][]int) bool { + m, n := len(s), len(p) + // base case + if j == n { + return i == m + } + if i == m { + if (n-j)%2 == 1 { + return false + } + for ; j+1 < n; j += 2 { + if p[j+1] != '*' { + return false + } + } + return true + } + // 查备忘录,防止重复计算 + if memo[i][j] != -1 { + return memo[i][j] == 1 + } + + var res bool + + if s[i] == p[j] || p[j] == '.' { + if j < n-1 && p[j+1] == '*' { + res = dp(s, i, p, j+2, memo) || dp(s, i+1, p, j, memo) + } else { + res = dp(s, i+1, p, j+1, memo) + } + } else { + if j < n-1 && p[j+1] == '*' { + res = dp(s, i, p, j+2, memo) + } + } + // 将当前结果记入备忘录 + if res { + memo[i][j] = 1 + } else { + memo[i][j] = 0 + } + return res +} +``` + +```java +// by chatGPT (java) +class Solution { + // 备忘录 + private int[][] memo; + + public boolean isMatch(String s, String p) { + int m = s.length(), n = p.length(); + memo = new int[m][n]; + for (int i = 0; i < m; i++) { + Arrays.fill(memo[i], -1); + } + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0); + } + + /* 计算 p[j..] 是否匹配 s[i..] */ + private boolean dp(String s, int i, String p, int j) { + int m = s.length(), n = p.length(); + // base case + if (j == n) { + return i == m; + } + if (i == m) { + if ((n - j) % 2 == 1) { + return false; + } + for (; j + 1 < n; j += 2) { + if (p.charAt(j + 1) != '*') { + return false; + } + } + return true; + } + + // 查备忘录,防止重复计算 + if (memo[i][j] != -1) { + return memo[i][j] == 1; + } + + boolean res = false; + + if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2) + || dp(s, i + 1, p, j); + } else { + res = dp(s, i + 1, p, j + 1); + } + } else { + if (j < n - 1 && p.charAt(j + 1) == '*') { + res = dp(s, i, p, j + 2); + } else { + res = false; + } + } + // 将当前结果记入备忘录 + memo[i][j] = res ? 1 : 0; + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var isMatch = function(s, p) { + var m = s.length, + n = p.length; + var memo = new Array(m + 1); // 先构造一个数组 + for (var i = 0; i <= m; i++) { + memo[i] = new Array(n + 1).fill(false); + } + memo[0][0] = true; // 两个字符串为空串时,互相匹配 + + for (i = 0; i <= m; i++) { + for (var j = 1; j <= n; j++) { + if (p.charAt(j - 1) === '*') { + // * 号能匹配 0 次或更多次 + memo[i][j] = memo[i][j - 2] // 匹配 0 次 + || (i > 0 && (s.charAt(i - 1) === p.charAt(j - 2) || p.charAt(j - 2) === '.') && memo[i - 1][j]); + } else { + if (i > 0 && (s.charAt(i - 1) === p.charAt(j - 1) || p.charAt(j - 1) === '.')) { + memo[i][j] = memo[i - 1][j - 1]; + } + } + } + } + + return memo[m][n]; // 返回从两个字符串的尾部开始,是否相互匹配 +}; +``` + +```python +# by chatGPT (python) +class Solution: + def isMatch(self, s: str, p: str) -> bool: + # 备忘录 + memo = [] + # 创建大小为m*n的备忘录,初始值为-1 + m = len(s) + n = len(p) + for i in range(m): + memo.append([-1] * n) + # 索引指针 i,j 从 0 开始移动 + return self.dp(s, 0, p, 0, memo) + + # 计算 p[j..] 是否匹配 s[i..] + def dp(self, s, i, p, j, memo): + m = len(s) + n = len(p) + # 如果 j 达到结尾,判断 i 是否也到结尾 + if j == n: + return i == m + # 如果 i 达到结尾,判断 j 接下来的字符能否匹配空串 + if i == m: + if (n - j) % 2 == 1: + return False + for k in range(j + 1, n, 2): + if p[k] != '*': + return False + return True + + # 查备忘录,防止重复计算 + if memo[i][j] != -1: + return memo[i][j] + + res = False + if s[i] == p[j] or p[j] == '.': + if j < n - 1 and p[j + 1] == '*': + res = self.dp(s, i, p, j + 2, memo) or self.dp(s, i + 1, p, j, memo) + else: + res = self.dp(s, i + 1, p, j + 1, memo) + else: + if j < n - 1 and p[j + 1] == '*': + res = self.dp(s, i, p, j + 2, memo) + else: + res = False + + # 记入当前结果到备忘录 + memo[i][j] = res + return res +``` + +https://leetcode.cn/problems/zheng-ze-biao-da-shi-pi-pei-lcof 的多语言解法👆 + +https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + // 存储 inorder 中值到索引的映射 + unordered_map valToIndex; + + TreeNode* buildTree(vector& preorder, vector& inorder) { + for (int i = 0; i < inorder.size(); i++) { + valToIndex[inorder[i]] = i; + } + return build(preorder, 0, preorder.size() - 1, + inorder, 0, inorder.size() - 1); + } + + /* + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 + */ + TreeNode* build(vector& preorder, int preStart, int preEnd, + vector& inorder, int inStart, int inEnd) { + if (preStart > preEnd) { + return nullptr; + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex[rootVal]; + + int leftSize = index - inStart; + + // 先构造出当前根节点 + TreeNode* root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/4.jpeg) + */ + // 递归构造左右子树 + root->left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + + root->right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); + return root; + } +}; +``` + +```go +// by chatGPT (go) +func buildTree(preorder []int, inorder []int) *TreeNode { + indexMap := make(map[int]int) + for i, val := range inorder { + indexMap[val] = i + } + return build(preorder, inorder, 0, len(preorder)-1, 0, len(inorder)-1, indexMap) +} + +func build(preorder []int, inorder []int, preStart int, preEnd int, inStart int, inEnd int, indexMap map[int]int) *TreeNode { + if preStart > preEnd { + return nil + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + rootVal := preorder[preStart] + // rootVal 在中序遍历数组中的索引 + index := indexMap[rootVal] + + leftSize := index - inStart + + // 先构造出当前根节点 + root := &TreeNode{Val: rootVal} + // 递归构造左右子树 + root.Left = build(preorder, inorder, preStart+1, preStart+leftSize, inStart, index-1, indexMap) + root.Right = build(preorder, inorder, preStart+leftSize+1, preEnd, index+1, inEnd, indexMap) + return root +} +``` + +```java +// by labuladong (java) +class Solution { + // 存储 inorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode buildTree(int[] preorder, int[] inorder) { + for (int i = 0; i < inorder.length; i++) { + valToIndex.put(inorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); + } + + /* + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 + */ + TreeNode build(int[] preorder, int preStart, int preEnd, + int[] inorder, int inStart, int inEnd) { + if (preStart > preEnd) { + return null; + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex.get(rootVal); + + int leftSize = index - inStart; + + // 先构造出当前根节点 + TreeNode root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/4.jpeg) + */ + // 递归构造左右子树 + root.left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + + root.right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); + return root; + } +} +``` + +```javascript +// by chatGPT (javascript) +var buildTree = function(preorder, inorder) { + // 存储 inorder 中值到索引的映射 + const valToIndex = new Map(); + for (let i = 0; i < inorder.length; i++) { + valToIndex.set(inorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); +}; + +/* + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 +*/ +function build(preorder, preStart, preEnd, + inorder, inStart, inEnd) { + if (preStart > preEnd) { + return null; + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + const rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + const index = valToIndex.get(rootVal); + + const leftSize = index - inStart; + + // 先构造出当前根节点 + const root = new TreeNode(rootVal); + /** + ![](../pictures/二叉树系列2/4.jpeg) + */ + // 递归构造左右子树 + root.left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + + root.right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); + return root; +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + # 存储 inorder 中值到索引的映射 + self.valToIndex = {} + + def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: + for i in range(len(inorder)): + self.valToIndex[inorder[i]] = i + return self.build(preorder, 0, len(preorder) - 1, + inorder, 0, len(inorder) - 1) + + ''' + 定义:前序遍历数组为 preorder[preStart..preEnd], + 中序遍历数组为 inorder[inStart..inEnd], + 构造这个二叉树并返回该二叉树的根节点 + ''' + + def build(self, preorder: List[int], preStart: int, preEnd: int, + inorder: List[int], inStart: int, inEnd: int) -> TreeNode: + if preStart > preEnd: + return None + + # root 节点对应的值就是前序遍历数组的第一个元素 + rootVal = preorder[preStart] + # rootVal 在中序遍历数组中的索引 + index = self.valToIndex[rootVal] + + leftSize = index - inStart + + # 先构造出当前根节点 + root = TreeNode(rootVal) + """ + + ![](../pictures/二叉树系列2/4.jpeg) + """ + + # 递归构造左右子树 + root.left = self.build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1) + + root.right = self.build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd) + return root +``` + +https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + vector permutation(string s) { + permuteUnique(s); + // 根据 res 容量构造返回数组 + vector arr = vector(res.size()); + for (int i = 0; i < res.size(); i++) { + arr[i] = res[i]; + } + return arr; + } + + // 保存所有的排列结果 + vector res; + // 排列的过程中记录一条排列路径 + string track; + // 标记在这个排列路径中哪些元素已经被访问过 + vector used; + + void permuteUnique(string nums) { + // 先对排列的元素进行排序,相同元素正着排列 + sort(nums.begin(), nums.end()); + // 初始化 used 数组为 false + used.resize(nums.size(), false); + backtrack(nums); + } + + void backtrack(string nums) { + // 如果生成的路径长度等于元素长度,这就是一条完整的排列路径,添加到 res 中 + if (track.length() == nums.length()) { + res.push_back(track); + return; + } + + // 枚举排列路径中所有未使用的元素 + for (int i = 0; i < nums.length(); i++) { + // 如果这个元素已经被访问过,直接跳过 + if (used[i]) { + continue; + } + // 使用这个元素时,应对通过访问顺序来避免其中相同元素顺序不同的重复排列 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + // 将这个元素添加到排列路径中 + track.push_back(nums[i]); + used[i] = true; + backtrack(nums); + track.pop_back(); + used[i] = false; + } + } +}; +``` + +```go +// by chatGPT (go) +import "sort" + +func permutation(s string) []string { + res := []string{} + track := []byte{} + used := make([]bool, len(s)) + nums := []byte(s) + sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] }) + backtrack(nums, &res, track, used) + return res +} + +func backtrack(nums []byte, res *[]string, track []byte, used []bool) { + if len(track) == len(nums) { + *res = append(*res, string(track)) + return + } + + for i := 0; i < len(nums); i++ { + if used[i] { + continue + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if i > 0 && nums[i] == nums[i-1] && !used[i-1] { + continue + } + track = append(track, nums[i]) + used[i] = true + backtrack(nums, res, track, used) + track = track[:len(track)-1] + used[i] = false + } +} +``` + +```java +// by labuladong (java) +class Solution { + public String[] permutation(String s) { + permuteUnique(s.toCharArray()); + String[] arr = new String[res.size()]; + for (int i = 0; i < res.size(); i++) { + arr[i] = res.get(i); + } + return arr; + } + + List res = new ArrayList<>(); + StringBuilder track = new StringBuilder(); + boolean[] used; + + public List permuteUnique(char[] nums) { + // 先排序,让相同的元素靠在一起 + Arrays.sort(nums); + used = new boolean[nums.length]; + backtrack(nums); + return res; + } + + void backtrack(char[] nums) { + if (track.length() == nums.length) { + res.add(track.toString()); + return; + } + + for (int i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + track.append(nums[i]); + used[i] = true; + backtrack(nums); + track.deleteCharAt(track.length() - 1); + used[i] = false; + } + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @return {string[]} + */ +var permutation = function(s) { + let res = []; + let track = []; + let used; + + var permuteUnique = function(nums) { + // 先排序,让相同的元素靠在一起 + nums.sort(); + used = new Array(nums.length).fill(false); + backtrack(nums); + return res; + }; + + var backtrack = function(nums) { + if (track.length === nums.length) { + res.push(track.join("")); + return; + } + + for (let i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) { + continue; + } + track.push(nums[i]); + used[i] = true; + backtrack(nums); + track.pop(); + used[i] = false; + } + }; + + permuteUnique(s.split("")); + let arr = new Array(res.size()); + for (let i = 0; i < res.size(); i++) { + arr[i] = res.get(i); + } + return arr; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def permutation(self, s: str) -> List[str]: + self.permuteUnique(list(s)) + # 将结果转化为字符串数组返回 + arr = [None] * len(self.res) + for i in range(len(self.res)): + arr[i] = self.res[i] + return arr + + # 存储结果的列表 + res = [] + # 存储每一次递归后得到的排列方案 + track = "" + # 记录每个字符是否被使用过 + used = [] + + def permuteUnique(self, nums: List[str]) -> None: + """ + :type nums: List[int] + :rtype: List[List[int]] + """ + # 先排序,让相同的元素靠在一起 + nums.sort() + # 初始化标记列表 + self.used = [False] * len(nums) + self.backtrack(nums) + + def backtrack(self, nums: List[str]) -> None: + if len(self.track) == len(nums): + self.res.append(self.track) + return + + for i in range(len(nums)): + if self.used[i]: + continue + # 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if i > 0 and nums[i] == nums[i - 1] and not self.used[i - 1]: + continue + self.track += nums[i] + self.used[i] = True + self.backtrack(nums) + self.track = self.track[:-1] + self.used[i] = False +``` + +https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof 的多语言解法👆 + +https://leetcode.cn/problems/zlDJc7 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + int openLock(vector& deadends, string target) { + // 记录需要跳过的死亡密码 + unordered_set deads(deadends.begin(), deadends.end()); + // 记录已经穷举过的密码,防止走回头路 + unordered_set visited; + queue q; + // 从起点开始启动广度优先搜索 + int step = 0; + q.push("0000"); + visited.emplace("0000"); + + while (!q.empty()) { + int sz = q.size(); + /* 将当前队列中的所有节点向周围扩散 */ + for (int i = 0; i < sz; i++) { + string cur = q.front(); + q.pop(); + + /* 判断是否到达终点 */ + if (deads.find(cur) != deads.end()) + continue; + if (cur == target) + return step; + + /* 将一个节点的未遍历相邻节点加入队列 */ + for (int j = 0; j < 4; j++) { + string up = plusOne(cur, j); + if (visited.find(up) == visited.end()) { + q.push(up); + visited.emplace(up); + } + string down = minusOne(cur, j); + if (visited.find(down) == visited.end()) { + q.push(down); + visited.emplace(down); + } + } + } + /* 在这里增加步数 */ + step++; + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1; + } + + // 将 s[j] 向上拨动一次 + string plusOne(string s, int j) { + if (s[j] == '9') + s[j] = '0'; + else + s[j] += 1; + return s; + } + + // 将 s[i] 向下拨动一次 + string minusOne(string s, int j) { + if (s[j] == '0') + s[j] = '9'; + else + s[j] -= 1; + return s; + } +}; +``` + +```go +// by chatGPT (go) +func openLock(deadends []string, target string) int { + // 记录需要跳过的死亡密码 + var deads = map[string]bool{} + for _, s := range deadends { + deads[s] = true + } + // 记录已经穷举过的密码,防止走回头路 + var visited = map[string]bool{} + q := []string{"0000"} + // 从起点开始启动广度优先搜索 + step := 0 + visited["0000"] = true + + for len(q) > 0 { + sz := len(q) + /* 将当前队列中的所有节点向周围扩散 */ + for i := 0; i < sz; i++ { + cur := q[0] + q = q[1:] + + /* 判断是否到达终点 */ + if deads[cur] { + continue + } + if cur == target { + return step + } + + /* 将一个节点的未遍历相邻节点加入队列 */ + for j := 0; j < 4; j++ { + up := plusOne(cur, j) + if !visited[up] { + q = append(q, up) + visited[up] = true + } + down := minusOne(cur, j) + if !visited[down] { + q = append(q, down) + visited[down] = true + } + } + } + /* 在这里增加步数 */ + step++ + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1 +} + +// 将 s[j] 向上拨动一次 +func plusOne(s string, j int) string { + ch := []byte(s) + if ch[j] == '9' { + ch[j] = '0' + } else { + ch[j] += 1 + } + return string(ch) +} + +// 将 s[i] 向下拨动一次 +func minusOne(s string, j int) string { + ch := []byte(s) + if ch[j] == '0' { + ch[j] = '9' + } else { + ch[j] -= 1 + } + return string(ch) +} +``` + +```java +// by labuladong (java) +class Solution { + public int openLock(String[] deadends, String target) { + // 记录需要跳过的死亡密码 + Set deads = new HashSet<>(); + for (String s : deadends) deads.add(s); + // 记录已经穷举过的密码,防止走回头路 + Set visited = new HashSet<>(); + Queue q = new LinkedList<>(); + // 从起点开始启动广度优先搜索 + int step = 0; + q.offer("0000"); + visited.add("0000"); + + while (!q.isEmpty()) { + int sz = q.size(); + /* 将当前队列中的所有节点向周围扩散 */ + for (int i = 0; i < sz; i++) { + String cur = q.poll(); + + /* 判断是否到达终点 */ + if (deads.contains(cur)) + continue; + if (cur.equals(target)) + return step; + + /* 将一个节点的未遍历相邻节点加入队列 */ + for (int j = 0; j < 4; j++) { + String up = plusOne(cur, j); + if (!visited.contains(up)) { + q.offer(up); + visited.add(up); + } + String down = minusOne(cur, j); + if (!visited.contains(down)) { + q.offer(down); + visited.add(down); + } + } + } + /* 在这里增加步数 */ + step++; + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1; + } + + // 将 s[j] 向上拨动一次 + String plusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '9') + ch[j] = '0'; + else + ch[j] += 1; + return new String(ch); + } + + // 将 s[i] 向下拨动一次 + String minusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '0') + ch[j] = '9'; + else + ch[j] -= 1; + return new String(ch); + } +} +``` + +```javascript +// by chatGPT (javascript) +var openLock = function (deadends, target) { + // 记录需要跳过的死亡密码 + const deads = new Set(deadends); + // 记录已经穷举过的密码,防止走回头路 + const visited = new Set(); + const q = []; + let step = 0; + // 从起点开始启动广度优先搜索 + q.push("0000"); + visited.add("0000"); + + while (q.length > 0) { + let sz = q.length; + /* 将当前队列中的所有节点向周围扩散 */ + for (let i = 0; i < sz; i++) { + let cur = q.shift(); + /* 判断是否到达终点 */ + if (deads.has(cur)) { + continue; + } + if (cur === target) { + return step; + } + /* 将一个节点的未遍历相邻节点加入队列 */ + for (let j = 0; j < 4; j++) { + let up = plusOne(cur, j); + if (!visited.has(up)) { + q.push(up); + visited.add(up); + } + let down = minusOne(cur, j); + if (!visited.has(down)) { + q.push(down); + visited.add(down); + } + } + } + /* 在这里增加步数 */ + step++; + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1; + + // 将 s[j] 向上拨动一次 + function plusOne(s, j) { + let ch = s.split(''); + if (ch[j] === '9') { + ch[j] = '0'; + } else { + ch[j] = (+ch[j] + 1).toString(); + } + return ch.join(''); + } + // 将 s[i] 向下拨动一次 + function minusOne(s, j) { + let ch = s.split(''); + if (ch[j] === '0') { + ch[j] = '9'; + } else { + ch[j] = (+ch[j] - 1).toString(); + } + return ch.join(''); + } +}; +``` + +```python +# by chatGPT (python) +class Solution: + def openLock(self, deadends: List[str], target: str) -> int: + #记录需要跳过的死亡密码 + deads = set(deadends) + #记录已经穷举过的密码,防止走回头路 + visited = set() + q = deque() + #从起点开始启动广度优先搜索 + step = 0 + q.append("0000") + visited.add("0000") + + while q: + sz = len(q) + #将当前队列中的所有节点向周围扩散 + for i in range(sz): + cur = q.popleft() + + #判断是否到达终点 + if cur in deads: + continue + if cur == target: + return step + + # 将一个节点的未遍历相邻节点加入队列 + for j in range(4): + up = self.plusOne(cur, j) + if up not in visited: + q.append(up) + visited.add(up) + down = self.minusOne(cur, j) + if down not in visited: + q.append(down) + visited.add(down) + + #在这里增加步数 + step += 1 + #如果穷举完都没找到目标密码,那就是找不到了 + return -1 + + + #将 s[j] 向上拨动一次 + def plusOne(self, s: str, j: int) -> str: + ch = list(s) + if ch[j] == '9': + ch[j] = '0' + else: + ch[j] = chr(ord(ch[j]) + 1) + return "".join(ch) + + #将 s[i] 向下拨动一次 + def minusOne(self, s: str, j: int) -> str: + ch = list(s) + if ch[j] == '0': + ch[j] = '9' + else: + ch[j] = chr(ord(ch[j]) - 1) + return "".join(ch) +``` + +https://leetcode.cn/problems/zlDJc7 的多语言解法👆 + +https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof 的多语言解法👇 + +```cpp +// by labuladong (cpp) +class Solution { + public: + int lengthOfLongestSubstring(string s) { + unordered_map window; + + int left = 0, right = 0; + int res = 0; // 记录结果 + while (right < s.size()) { + char c = s[right]; + right++; + // 进行窗口内数据的一系列更新 + window[c]++; + // 判断左侧窗口是否要收缩 + while (window[c] > 1) { + char d = s[left]; + left++; + // 进行窗口内数据的一系列更新 + window[d]--; + } + // 在这里更新答案 + res = max(res, right - left); + } + return res; + } +}; +``` + +```go +// by chatGPT (go) +/** + * @param s: a string + * @return: the length of the longest substring without repeating characters + */ +func lengthOfLongestSubstring(s string) int { + window := make(map[byte]int) + + left, right := 0, 0 + res := 0 // 记录结果 + for right < len(s) { + c := s[right] + right++ + // 进行窗口内数据的一系列更新 + window[c]++ + // 判断左侧窗口是否要收缩 + for window[c] > 1 { + d := s[left] + left++ + // 进行窗口内数据的一系列更新 + window[d]-- + } + // 在这里更新答案 + res = max(res, right - left) + } + return res +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```java +// by chatGPT (java) +class Solution { + public int lengthOfLongestSubstring(String s) { + Map window = new HashMap<>(); + + int left = 0, right = 0; + int res = 0; // 记录结果 + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + window.put(c, window.getOrDefault(c, 0) + 1); + // 判断左侧窗口是否要收缩 + while (window.get(c) > 1) { + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + window.put(d, window.get(d) - 1); + } + // 在这里更新答案 + res = Math.max(res, right - left); + } + return res; + } +} +``` + +```javascript +// by chatGPT (javascript) +var lengthOfLongestSubstring = function(s) { + // 哈希表记录窗口中字符出现的次数 + var window = new Map(); + // 左指针、右指针 + var left = 0, right = 0; + // 最长子串长度 + var res = 0; + while (right < s.length) { + // 移动右指针,扩大窗口 + var c = s.charAt(right); + right++; + // 更新哈希表 + if(window.has(c)){ + window.set(c, window.get(c) + 1); + } else { + window.set(c, 1); + } + // 如果当前字符在窗口中已经出现过,则考虑收缩窗口 + while (window.get(c) > 1) { + // 移动左指针,收缩窗口 + var d = s.charAt(left); + left++; + // 更新哈希表 + window.set(d, window.get(d) - 1); + } + // 更新最长子串长度 + res = Math.max(res, right - left); + } + return res; +}; +``` + +```python +# by chatGPT (python) +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + window = {} # 用于记录窗口中各字符出现的次数 + left = right = res = 0 # left为窗口左边界,right为窗口右边界,[left, right)为当前窗口,res用来记录最长无重复子串的长度 + + while right < len(s): + c = s[right] + right += 1 + window[c] = window.get(c, 0) + 1 # 将字符c的出现次数加1 + + while window[c] > 1: # 当右侧窗口中的字符c的个数大于1时,说明当前窗口中不符合条件,需要进行调整 + d = s[left] + left += 1 + window[d] -= 1 # 将窗口左侧的字符d移出窗口,其在window中的出现次数减1 + + res = max(res, right - left) # 更新最长无重复子串的长度 + + return res +``` + +https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof 的多语言解法👆 + +https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +// 二叉堆的解法思路 +class Solution1 { +public: + vector getLeastNumbers(vector& arr, int k) { + // 大顶堆,堆顶是最大元素 + priority_queue pq; + for (int e : arr) { + // 每个元素都要过一遍二叉堆 + pq.push(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.pop(); + } + } + // pq 中剩下的是 arr 中最小的 k 个元素 + vector res(k); + int i = 0; + while (!pq.empty()) { + res[i] = pq.top(); + pq.pop(); + i++; + } + return res; + } +}; + +// 快速选择的解法思路 +class Solution { +public: + vector getLeastNumbers(vector& arr, int k) { + vector res(k); + // 注意此题的 k 是元素个数而不是索引,所以和索引 p 做比较时要 - 1 + // 首先随机打乱数组 + shuffle(arr); + int lo = 0, hi = arr.size() - 1; + // 现在开始寻找第 k 大的元素 + while (lo <= hi) { + // 在 arr[lo..hi] 中选一个分界点 + int p = partition(arr, lo, hi); + if (p < k - 1) { + // 第 k 大的元素在 arr[p+1..hi] 中 + lo = p + 1; + } else if (p > k - 1) { + // 第 k 大的元素在 arr[lo..p-1] 中 + hi = p - 1; + } else { + // arr[p] 就是第 k 大元素,又因为快速排序的性质, + // arr[p] 左边的元素都比 arr[p] 小,所以现在 arr[0..k] 就是我们要找的答案 + for (int i = 0; i < k; i++) { + res[i] = arr[i]; + } + return res; + } + } + return res; + } + + // 对 nums[lo..hi] 进行切分 + int partition(vector& nums, int lo, int hi) { + int pivot = nums[lo]; + // 关于区间的边界控制需格外小心,稍有不慎就会出错 + // 我这里把 i, j 定义为开区间,同时定义: + // [lo, i) <= pivot;(j, hi] > pivot + // 之后都要正确维护这个边界区间的定义 + int i = lo + 1, j = hi; + // 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖 + while (i <= j) { + while (i < hi && nums[i] <= pivot) { + i++; + // 此 while 结束时恰好 nums[i] > pivot + } + while (j > lo && nums[j] > pivot) { + j--; + // 此 while 结束时恰好 nums[j] <= pivot + } + + if (i >= j) { + break; + } + // 此时 [lo, i) <= pivot && (j, hi] > pivot + // 交换 nums[j] 和 nums[i] + swap(nums[i], nums[j]); + // 此时 [lo, i] <= pivot && [j, hi] > pivot + } + // 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大 + swap(nums[lo], nums[j]); + return j; + } + + // 洗牌算法,将输入的数组随机打乱 + void shuffle(vector& nums) { + srand((unsigned)time(NULL)); + int n = nums.size(); + for (int i = 0; i < n; i++) { + // 生成 [i, n - 1] 的随机数 + int r = i + rand() % (n - i); + swap(nums[i], nums[r]); + } + } + + // 原地交换数组中的两个元素 + void swap(int& a, int& b) { + int temp = a; + a = b; + b = temp; + } +}; +``` + +```go +// by chatGPT (go) +// 二叉堆的解法思路 +func getLeastNumbers(arr []int, k int) []int { + // 大顶堆,堆顶是最大元素 + pq := make(IntHeap, 0) + heap.Init(&pq) + for _, e := range arr { + // 每个元素都要过一遍二叉堆 + heap.Push(&pq, e) + // 堆中元素多于 k 个时,删除堆顶元素 + if pq.Len() > k { + heap.Pop(&pq) + } + } + // pq 中剩下的是 arr 中最小的 k 个元素 + res := make([]int, k) + i := 0 + for pq.Len() > 0 { + res[i] = heap.Pop(&pq).(int) + i++ + } + return res +} + +type IntHeap []int + +func (h IntHeap) Len() int { return len(h) } +func (h IntHeap) Less(i, j int) bool { return h[i] > h[j] } +func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *IntHeap) Push(x interface{}) { + *h = append(*h, x.(int)) +} + +func (h *IntHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} + +// 快速选择的解法思路 +func getLeastNumbers(arr []int, k int) []int { + res := make([]int, k) + // 注意此题的 k 是元素个数而不是索引,所以和索引 p 做比较时要 - 1 + // 首先随机打乱数组 + shuffle(arr) + lo, hi := 0, len(arr)-1 + // 现在开始寻找第 k 大的元素 + for lo <= hi { + // 在 arr[lo..hi] 中选一个分界点 + p := partition(arr, lo, hi) + if p < k-1 { + // 第 k 大的元素在 arr[p+1..hi] 中 + lo = p + 1 + } else if p > k-1 { + // 第 k 大的元素在 arr[lo..p-1] 中 + hi = p - 1 + } else { + // arr[p] 就是第 k 大元素,又因为快速排序的性质, + // arr[p] 左边的元素都比 arr[p] 小,所以现在 arr[0..k] 就是我们要找的答案 + copy(res, arr[:k]) + return res + } + } + return res +} + +// 对 nums[lo..hi] 进行切分 +func partition(nums []int, lo, hi int) int { + pivot := nums[lo] + // 关于区间的边界控制需格外小心,稍有不慎就会出错 + // 我这里把 i, j 定义为开区间,同时定义: + // [lo, i) <= pivot;(j, hi] > pivot + // 之后都要正确维护这个边界区间的定义 + i, j := lo+1, hi + // 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖 + for i <= j { + for i < hi && nums[i] <= pivot { + i++ + // 此 while 结束时恰好 nums[i] > pivot + } + for j > lo && nums[j] > pivot { + j-- + // 此 while 结束时恰好 nums[j] <= pivot + } + + if i >= j { + break + } + // 此时 [lo, i) <= pivot && (j, hi] > pivot + // 交换 nums[j] 和 nums[i] + nums[i], nums[j] = nums[j], nums[i] + // 此时 [lo, i] <= pivot && [j, hi] > pivot + } + // 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大 + nums[lo], nums[j] = nums[j], nums[lo] + return j +} + +// 洗牌算法,将输入的数组随机打乱 +func shuffle(nums []int) { + rand.Seed(time.Now().UnixNano()) + n := len(nums) + for i := 0; i < n; i++ { + // 生成 [i, n - 1] 的随机数 + r := i + rand.Intn(n-i) + nums[i], nums[r] = nums[r], nums[i] + } +} +``` + +```java +// by labuladong (java) +// 二叉堆的解法思路 +class Solution1 { + public int[] getLeastNumbers(int[] arr, int k) { + // 大顶堆,堆顶是最大元素 + PriorityQueue pq = new PriorityQueue<>((a, b) -> { + return b - a; + }); + for (int e : arr) { + // 每个元素都要过一遍二叉堆 + pq.offer(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.poll(); + } + } + // pq 中剩下的是 arr 中最小的 k 个元素 + int[] res = new int[k]; + int i = 0; + while (!pq.isEmpty()) { + res[i] = pq.poll(); + i++; + } + return res; + } +} + +// 快速选择的解法思路 +class Solution { + public int[] getLeastNumbers(int[] arr, int k) { + int[] res = new int[k]; + // 注意此题的 k 是元素个数而不是索引,所以和索引 p 做比较时要 - 1 + // 首先随机打乱数组 + shuffle(arr); + int lo = 0, hi = arr.length - 1; + // 现在开始寻找第 k 大的元素 + while (lo <= hi) { + // 在 arr[lo..hi] 中选一个分界点 + int p = partition(arr, lo, hi); + if (p < k - 1) { + // 第 k 大的元素在 arr[p+1..hi] 中 + lo = p + 1; + } else if (p > k - 1) { + // 第 k 大的元素在 arr[lo..p-1] 中 + hi = p - 1; + } else { + // arr[p] 就是第 k 大元素,又因为快速排序的性质, + // arr[p] 左边的元素都比 arr[p] 小,所以现在 arr[0..k] 就是我们要找的答案 + for (int i = 0; i < k; i++) { + res[i] = arr[i]; + } + return res; + } + } + return res; + } + + // 对 nums[lo..hi] 进行切分 + private static int partition(int[] nums, int lo, int hi) { + int pivot = nums[lo]; + // 关于区间的边界控制需格外小心,稍有不慎就会出错 + // 我这里把 i, j 定义为开区间,同时定义: + // [lo, i) <= pivot;(j, hi] > pivot + // 之后都要正确维护这个边界区间的定义 + int i = lo + 1, j = hi; + // 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖 + while (i <= j) { + while (i < hi && nums[i] <= pivot) { + i++; + // 此 while 结束时恰好 nums[i] > pivot + } + while (j > lo && nums[j] > pivot) { + j--; + // 此 while 结束时恰好 nums[j] <= pivot + } + + if (i >= j) { + break; + } + // 此时 [lo, i) <= pivot && (j, hi] > pivot + // 交换 nums[j] 和 nums[i] + swap(nums, i, j); + // 此时 [lo, i] <= pivot && [j, hi] > pivot + } + // 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大 + swap(nums, lo, j); + return j; + } + + // 洗牌算法,将输入的数组随机打乱 + private static void shuffle(int[] nums) { + Random rand = new Random(); + int n = nums.length; + for (int i = 0 ; i < n; i++) { + // 生成 [i, n - 1] 的随机数 + int r = i + rand.nextInt(n - i); + swap(nums, i, r); + } + } + + // 原地交换数组中的两个元素 + private static void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } +} +``` + +```javascript +// by chatGPT (javascript) +var getLeastNumbers = function(arr, k) { + // 大顶堆,堆顶是最大元素 + let pq = new PriorityQueue((a, b) => { + return b - a; + }); + for (let e of arr) { + // 每个元素都要过一遍二叉堆 + pq.offer(e); + // 堆中元素多于 k 个时,删除堆顶元素 + if (pq.size() > k) { + pq.poll(); + } + } + // pq 中剩下的是 arr 中最小的 k 个元素 + let res = new Array(k); + let i = 0; + while (pq.size() > 0) { + res[i] = pq.poll(); + i++; + } + return res; +} + +class PriorityQueue { + constructor(compareFn = (a, b) => a - b) { + this.compareFn = compareFn; + this.heap = []; + } + + // 获取堆的大小 + size() { + return this.heap.length; + } + + // 获取堆顶元素 + peek() { + if (this.heap.length === 0) { + return null; + } + return this.heap[0]; + } + + // 删除堆顶元素 + poll() { + if (this.heap.length === 0) { + return null; + } + const top = this.heap[0]; + const last = this.heap.pop(); + if (this.heap.length > 0) { + this.heap[0] = last; + this.siftDown(0); + } + return top; + } + + // 向堆中插入一个元素 + offer(elem) { + this.heap.push(elem); + this.siftUp(this.heap.length - 1); + } + + // 元素下滤操作 + siftDown(k) { + while (2 * k + 1 < this.heap.length) { + let j = 2 * k + 1; + if (j + 1 < this.heap.length && this.compareFn(this.heap[j + 1], this.heap[j]) < 0) { + j++; + } + if (this.compareFn(this.heap[k], this.heap[j]) <= 0) { + break; + } + this.swap(k, j); + k = j; + } + } + + // 元素上滤操作 + siftUp(k) { + while (k > 0 && this.compareFn(this.heap[k], this.heap[Math.floor((k - 1) / 2)]) < 0) { + this.swap(k, Math.floor((k - 1) / 2)); + k = Math.floor((k - 1) / 2); + } + } + + // 交换堆中的两个元素 + swap(i, j) { + const temp = this.heap[i]; + this.heap[i] = this.heap[j]; + this.heap[j] = temp; + } +} + +var getLeastNumbers = function(arr, k) { + let res = new Array(k); + // 注意此题的 k 是元素个数而不是索引,所以和索引 p 做比较时要 - 1 + // 首先随机打乱数组 + shuffle(arr); + let lo = 0, hi = arr.length - 1; + // 现在开始寻找第 k 大的元素 + while (lo <= hi) { + // 在 arr[lo..hi] 中选一个分界点 + let p = partition(arr, lo, hi); + if (p < k - 1) { + // 第 k 大的元素在 arr[p+1..hi] 中 + lo = p + 1; + } else if (p > k - 1) { + // 第 k 大的元素在 arr[lo..p-1] 中 + hi = p - 1; + } else { + // arr[p] 就是第 k 大元素,又因为快速排序的性质, + // arr[p] 左边的元素都比 arr[p] 小,所以现在 arr[0..k] 就是我们要找的答案 + for (let i = 0; i < k; i++) { + res[i] = arr[i]; + } + return res; + } + } + return res; +} + +// 对 nums[lo..hi] 进行切分 +function partition(nums, lo, hi) { + let pivot = nums[lo]; + // 关于区间的边界控制需格外小心,稍有不慎就会出错 + // 我这里把 i, j 定义为开区间,同时定义: + // [lo, i) <= pivot;(j, hi] > pivot + // 之后都要正确维护这个边界区间的定义 + let i = lo + 1, j = hi; + // 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖 + while (i <= j) { + while (i < hi && nums[i] <= pivot) { + i++; + // 此 while 结束时恰好 nums[i] > pivot + } + while (j > lo && nums[j] > pivot) { + j--; + // 此 while 结束时恰好 nums[j] <= pivot + } + + if (i >= j) { + break; + } + // 此时 [lo, i) <= pivot && (j, hi] > pivot + // 交换 nums[j] 和 nums[i] + swap(nums, i, j); + // 此时 [lo, i] <= pivot && [j, hi] > pivot + } + // 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大 + swap(nums, lo, j); + return j; +} + +// 洗牌算法,将输入的数组随机打乱 +function shuffle(nums) { + let rand = new Random(); + let n = nums.length; + for (let i = 0; i < n; i++) { + // 生成 [i, n - 1] 的随机数 + let r = i + rand.nextInt(n - i); + swap(nums, i, r); + } +} + +// 原地交换数组中的两个元素 +function swap(nums, i, j) { + let temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; +} + +class Random { + nextInt(bound) { + return Math.floor(Math.random() * bound); + } +} +``` + +```python +# by chatGPT (python) +# 二叉堆的解法思路 +class Solution1: + def getLeastNumbers(self, arr: List[int], k: int) -> List[int]: + # 大顶堆,堆顶是最大元素 + pq = [] + for e in arr: + # 每个元素都要过一遍二叉堆 + heapq.heappush(pq, -e) + # 堆中元素多于 k 个时,删除堆顶元素 + if len(pq) > k: + heapq.heappop(pq) + # pq 中剩下的是 arr 中最小的 k 个元素 + res = [] + while pq: + res.append(-heapq.heappop(pq)) + return res[::-1] + +# 快速选择的解法思路 +class Solution: + def getLeastNumbers(self, arr: List[int], k: int) -> List[int]: + res = [0] * k + + # 注意此题的 k 是元素个数而不是索引,所以和索引 p 做比较时要 - 1 + # 首先随机打乱数组 + self.shuffle(arr) + lo, hi = 0, len(arr) - 1 + # 现在开始寻找第 k 大的元素 + while lo <= hi: + # 在 arr[lo..hi] 中选一个分界点 + p = self.partition(arr, lo, hi) + if p < k - 1: + # 第 k 大的元素在 arr[p+1..hi] 中 + lo = p + 1 + elif p > k - 1: + # 第 k 大的元素在 arr[lo..p-1] 中 + hi = p - 1 + else: + # arr[p] 就是第 k 大元素,又因为快速排序的性质, + # arr[p] 左边的元素都比 arr[p] 小,所以现在 arr[0..k] 就是我们要找的答案 + for i in range(k): + res[i] = arr[i] + return res + + return res + + # 对 nums[lo..hi] 进行切分 + @staticmethod + def partition(nums: List[int], lo: int, hi: int) -> int: + pivot = nums[lo] + # 关于区间的边界控制需格外小心,稍有不慎就会出错 + # 我这里把 i, j 定义为开区间,同时定义: + # [lo, i) <= pivot;(j, hi] > pivot + # 之后都要正确维护这个边界区间的定义 + i, j = lo + 1, hi + # 当 i > j 时结束循环,以保证区间 [lo, hi] 都被覆盖 + while i <= j: + while i < hi and nums[i] <= pivot: + i += 1 + # 此 while 结束时恰好 nums[i] > pivot + while j > lo and nums[j] > pivot: + j -= 1 + # 此 while 结束时恰好 nums[j] <= pivot + + if i >= j: + break + # 此时 [lo, i) <= pivot && (j, hi] > pivot + # 交换 nums[j] 和 nums[i] + nums[i], nums[j] = nums[j], nums[i] + # 此时 [lo, i] <= pivot && [j, hi] > pivot + + # 最后将 pivot 放到合适的位置,即 pivot 左边元素较小,右边元素较大 + nums[lo], nums[j] = nums[j], nums[lo] + return j + + # 洗牌算法,将输入的数组随机打乱 + @staticmethod + def shuffle(nums: List[int]) -> None: + n = len(nums) + for i in range(n): + # 生成 [i, n - 1] 的随机数 + r = i + random.randint(0, n - i - 1) + nums[i], nums[r] = nums[r], nums[i] +``` + +https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof 的多语言解法👆 + +https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + string reverseLeftWords(string s, int n) { + return s.substr(n) + s.substr(0, n); + } +}; +``` + +```go +// by chatGPT (go) +func reverseLeftWords(s string, n int) string { + return s[n:] + s[:n] +} +``` + +```java +// by labuladong (java) +class Solution { + public String reverseLeftWords(String s, int n) { + return s.substring(n) + s.substring(0, n); + } +} +``` + +```javascript +// by chatGPT (javascript) +/** + * @param {string} s + * @param {number} n + * @return {string} + */ +var reverseLeftWords = function(s, n) { + return s.substring(n) + s.substring(0, n); +}; +``` + +```python +# by chatGPT (python) +class Solution: + def reverseLeftWords(self, s: str, n: int) -> str: + return s[n:] + s[:n] +``` + +https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof 的多语言解法👆 \ No newline at end of file diff --git "a/\346\212\200\346\234\257/linuxshell.md" "b/\346\212\200\346\234\257/linuxshell.md" index 7001f55505..40770abe70 100644 --- "a/\346\212\200\346\234\257/linuxshell.md" +++ "b/\346\212\200\346\234\257/linuxshell.md" @@ -1,6 +1,18 @@ +# 关于 Linux shell 你必须知道的技巧 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + 我个人很喜欢使用 Linux 系统,虽然说 Windows 的图形化界面做的确实比 Linux 好,但是对脚本的支持太差了。一开始有点不习惯命令行操作,但是熟悉了之后反而发现移动鼠标点点点才是浪费时间的罪魁祸首。。。 -**那么对于 Linux 命令行,本文不是介绍某些命令的用法,而是说明一些简单却特别容易让人迷惑的细节问题**。 +**那么对于 Linux 命令行,本文不是介绍某些命令的具体用法,而是结合使用场景说明一些容易让人迷惑的细节问题和能够提升效率的小技巧**。 1、标准输入和命令参数的区别。 @@ -10,6 +22,8 @@ 4、有的命令和`sudo`一起用就 command not found。 +5、避免输入重复的文件名、重复的路径、重复的命令的方法,以及一些其他小技巧。 + ### 一、标准输入和参数的区别 这个问题一定是最容易让人迷惑的,具体来说,就是搞不清什么时候用管道符`|`和文件重定向`>`,`<`,什么时候用变量`$`。 @@ -37,7 +51,7 @@ $ rm $(where connect.sh) **标准输入就是编程语言中诸如`scanf`或者`readline`这种命令;而参数是指程序的`main`函数传入的`args`字符数组**。 -前文「Linux文件描述符」说过,管道符和重定向符是将数据作为程序的标准输入,而`$(cmd)`是读取`cmd`命令输出的数据作为参数。 +前文 [Linux文件描述符](https://labuladong.online/algo/fname.html?fname=linux进程) 说过,管道符和重定向符是将数据作为程序的标准输入,而`$(cmd)`是读取`cmd`命令输出的数据作为参数。 用刚才的例子说,`rm`命令源代码中肯定不接受标准输入,而是接收命令行参数,删除相应的文件。作为对比,`cat`命令是既接受标准输入,又接受命令行参数: @@ -85,7 +99,7 @@ $ logout 类似的,还有一种后台运行常用的做法是这样: ```shell -$ nohub some_cmd & +$ nohup some_cmd & ``` `nohub`命令也是类似的原理,不过通过我的测试,还是`(cmd &)`这种形式更加稳定。 @@ -96,7 +110,7 @@ $ nohub some_cmd & shell 的行为可以测试,使用`set -x`命令,会开启 shell 的命令回显,你可以通过回显观察 shell 到底在执行什么命令: -![](../pictures/linuxshell/1.png) +![](https://labuladong.online/algo/images/linuxshell/1.png) 可见 `echo $(cmd)` 和 `echo "$(cmd)"`,结果差不多,但是仍然有区别。注意观察,双引号转义完成的结果会自动增加单引号,而前者不会。 @@ -104,7 +118,7 @@ shell 的行为可以测试,使用`set -x`命令,会开启 shell 的命令 ### 四、sudo 找不到命令 -有时候我们普通用户可以用的命令,用`sudo`加权限之后却报错 command not found: +有时候我们普通用户可以用的命令,用 `sudo` 加权限之后却报错 command not found: ```shell $ connect.sh @@ -114,14 +128,14 @@ $ sudo connect.sh sudo: command not found ``` -原因在于,`connect.sh`这个脚本仅存在于该用户的环境变量中: +原因在于,`connect.sh` 这个脚本仅存在于该用户的环境变量中: ```shell $ where connect.sh /home/fdl/bin/connect.sh ``` -**当使用`sudo`时,系统认为是 root 用户在执行命令,所以会去搜索 root 用户的环境变量**,而这个脚本在 root 的环境变量目录中当然是找不到的。 +**当使用 `sudo` 时,系统会使用 `/etc/sudoers` 这个文件中规定的该用户的权限和环境变量**,而这个脚本在 `/etc/sudoers` 环境变量目录中当然是找不到的。 解决方法是使用脚本文件的路径,而不是仅仅通过脚本名称: @@ -129,13 +143,218 @@ $ where connect.sh $ sudo /home/fdl/bin/connect.sh ``` -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: +### 五、输入相似文件名太麻烦 + +用花括号括起来的字符串用逗号连接,可以自动扩展,非常有用,直接看例子: + +```shell +$ echo {one,two,three}file +onefile twofile threefile + +$ echo {one,two,three}{1,2,3} +one1 one2 one3 two1 two2 two3 three1 three2 three3 +``` + +你看,花括号中的每个字符都可以和之后(或之前)的字符串进行组合拼接,**注意花括号和其中的逗号不可以用空格分隔,否则会被认为是普通的字符串对待**。 + +这个技巧有什么实际用处呢?最简单有用的就是给 `cp`, `mv`, `rm` 等命令扩展参数: + +```shell +$ cp /very/long/path/file{,.bak} +# 给 file 复制一个叫做 file.bak 的副本 + +$ rm file{1,3,5}.txt +# 删除 file1.txt file3.txt file5.txt + +$ mv *.{c,cpp} src/ +# 将所有 .c 和 .cpp 为后缀的文件移入 src 文件夹 +``` + +### 六、输入路径名称太麻烦 + +**用 `cd -` 返回刚才呆的目录**,直接看例子吧: + +```shell +$ pwd +/very/long/path +# 回到家目录瞅瞅 +$ cd +$ pwd +/home/labuladong +# 再返回刚才那个目录 +$ cd - +$ pwd +/very/long/path +``` + +**特殊命令 `!$` 会替换成上一次命令最后的路径**,直接看例子: + +```shell +# 没有加可执行权限 +$ /usr/bin/script.sh +zsh: permission denied: /usr/bin/script.sh + +$ chmod +x !$ +chmod +x /usr/bin/script.sh +``` + +**特殊命令 `!*` 会替换成上一次命令输入的所有文件路径**,直接看例子: + +```shell +# 创建了三个脚本文件 +$ file script1.sh script2.sh script3.sh + +# 给它们全部加上可执行权限 +$ chmod +x !* +chmod +x script1.sh script2.sh script3.sh +``` + +**可以在环境变量 `CDPATH` 中加入你常用的工作目录**,当 `cd` 命令在当前目录中找不到你指定的文件/目录时,会自动到 `CDPATH` 中的目录中寻找。 + +比如说我常去 `/var/log` 目录找日志,可以执行如下命令: + +```shell +$ export CDPATH='~:/var/log' +# cd 命令将会在 ~ 目录和 /var/log 目录扩展搜索 + +$ pwd +/home/labuladong/musics +$ cd mysql +cd /var/log/mysql +$ pwd +/var/log/mysql +$ cd my_pictures +cd /home/labuladong/my_pictures +``` + +这个技巧是十分好用的,这样就免了经常写完整的路径名称,节约不少时间。 + +需要注意的是,以上操作是 bash 支持的,其他主流 shell 解释器当然都支持扩展 `cd` 命令的搜索目录,但可能不是修改 `CDPATH` 这个变量,具体的设置方法可以自行搜索。 + +### 七、输入重复命令太麻烦 + +**使用特殊命令 `!!`,可以自动替换成上一次使用的命令**: + +```shell +$ apt install net-tools +E: Could not open lock file - open (13: Permission denied) + +$ sudo !! +sudo apt install net-tools +[sudo] password for fdl: +``` + +有的命令很长,一时间想不起来具体参数了怎么办? + +**对于 bash 终端,可以使用 `Ctrl+R` 快捷键反向搜索历史命令**,之所以说是反向搜索,就是搜索最近一次输入的命令。 + +比如按下 `Ctrl+R` 之后,输入 `sudo`,bash 就会搜索出最近一次包含 `sudo` 的命令,你回车之后就可以运行该命令了: + +```shell +(reverse-i-search)`sudo': sudo apt install git +``` + +但是这个方法有缺点:首先,该功能似乎只有 bash 支持,我用的 zsh 作为 shell 终端,就用不了;第二,只能查找出一个(最近的)命令,如果我想找以前的某个命令,就没办法了。 + +对于这种情况,**我们最常用的方法是使用 `history` 命令配合管道符和 `grep` 命令来寻找某个历史命令**: + +```shell +# 过滤出所有包含 config 字段的历史命令 +$ history | grep 'config' + 7352 ./configure + 7434 git config --global --unset https.proxy + 9609 ifconfig + 9985 clip -o | sed -z 's/\n/,\n/g' | clip +10433 cd ~/.config +``` +你使用的所有 shell 命令都会被记录,前面的数字就表示这是第几个命令,找到你想重复使用的命令后,也不需要复制粘贴该命令,**只要使用 `!` + 你想重用的命令编号即可运行该命令**。 + +拿上面的例子,我想重新运行 `git config` 那条命令,就可以这样: + +```shell +$ !7434 +git config --global --unset https.proxy +# 运行完成 +``` + +我觉得 `history` 加管道加 `grep` 这样打的字还是太多,可以在 你的 shell 配置文件中(`.bashrc`,`.zshrc` 等) 中写这样一个函数: + +```shell +his() +{ + history | grep "$@" +} +``` + +这样就不需要写那么多,只需要 `his 'some_keyword'` 即可搜索历史命令。 + +我一般不使用 bash 作为终端,我给大家推荐一款很好用的 shell 终端叫做 zsh,这也是我自己使用的 shell。这款终端还可以扩展各种插件,非常好用,具体配置方法可自行搜索。 + +### 其他小技巧 + +**1、`yes` 命令自动输入字符 `y` 进行确认**: + +我们安装某些软件的时候,可能有交互式的提问: + +```shell +$ sudo apt install XXX +... +XXX will use 996 MB disk space, continue? [y/n] +``` + +一般情况下我们都是一路 y 到底,但如果我们想自动化一些软件的安装就很烦,遇到这种交互式提问就卡住了,还得手动处理。 + +`yes` 命令可以帮助我们: + +```shell +$ yes | your_cmd +``` + +这样就会一路自动 `y` 下去,不会停下让我们输入了。 + +如果你读过前文 [Linux 文件描述符](https://labuladong.online/algo/fname.html?fname=linux进程),就知道其原理很简单: + +你单独运行一下 `yes` 命令,发现它就是打印出一大堆字符 y,通过管道把输出和 `your_cmd` 的标准输入相连接,如果 `your_cmd` 又提出无聊的问题,就会从标准输入读取数据,也就会读取到一个 y 和换行符,和你手动输入 y 确认是一个效果。 + +**2、特殊变量 `$?` 记录上一次命令的返回值**。 + +在 Linux shell 中,遵循 C 语言的习惯,返回值为 0 的话就是程序正常退出,非 0 值就是异常退出出。读取上一次命令的返回值在平时使用命令行时感觉没什么用,但是如果你想编写一些 shell 脚本,知道返回值非常有用。 + +**举个实际的例子**,比如我的 Github 仓库 fucking-algorithm ,我需要给其中所有 markdown 文件最下方添加上一篇、下一篇、目录三个页脚链接,有的文章已经有了页脚,大部分都没有。 + +为了防止重复添加,我必须知道一个 md 文件下方是否已添加,这时候就可以使用 `$?` 变量配合 `grep` 命令做到: + +```shell +#!/bin/bash +filename=$1 +# 查看文件尾部是否包含关键词 +tail | grep '下一篇' $filename +# grep 查找到匹配会返回 0,找不到则返回非 0 值 +[ $? -ne 0 ] && { 添加页脚; } +``` + +**3、特殊变量 `$$` 记录当前进程的 PID**。 + +这个功能可能在平时使用时也不怎么用,但是在写 shell 脚本时也非常有用,比如说你要在 `/tmp` 创建临时文件,给文件起名字一直都是非常让人费脑子的,这时候可以使用 `$$` 变量扩展出当前进程的 PID 作为临时文件名,PID 在计算机中都是唯一的,所以绝不会重复,也不需要你记住临时文件的名字。 + +好了,今天就分享这些技巧吧,如果大家对 Linux 有兴趣,可以点在看分享,数据不错的话下次再写点。 + + + +
+
+引用本文的文章 + + - [Linux 管道符的坑](https://labuladong.online/algo/fname.html?fname=linux技巧3) + +

+ + + -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:一文看懂 session 和 cookie](../技术/session和cookie.md) -[下一篇:加密算法的前身今世](../技术/密码技术.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" "b/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" index 2d6c6ba879..cd111459fd 100644 --- "a/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" +++ "b/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" @@ -1,5 +1,15 @@ # Linux的进程、线程、文件描述符是什么 + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + 说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案:**在 Linux 系统中,进程和线程几乎没有区别**。 Linux 中的进程就是一个数据结构,看明白就可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从操作系统的角度看看为什么说线程和进程基本没有区别。 @@ -8,7 +18,7 @@ Linux 中的进程就是一个数据结构,看明白就可以理解文件描 首先,抽象地来说,我们的计算机就是这个东西: -![](../pictures/linuxProcess/1.jpg) +![](https://labuladong.online/algo/images/linuxProcess/1.jpg) 这个大的矩形表示计算机的**内存空间**,其中的小矩形代表**进程**,左下角的圆形表示**磁盘**,右下角的图形表示一些**输入输出设备**,比如鼠标键盘显示器等等。另外,注意到内存空间被划分为了两块,上半部分表示**用户空间**,下半部分表示**内核空间**。 @@ -39,45 +49,46 @@ struct task_struct { }; ``` -`task_struct`就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。源码比较复杂,我这里就截取了一小部分比较常见的。 + `task_struct` 就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。源码比较复杂,我这里就截取了一小部分比较常见的。 -其中比较有意思的是`mm`指针和`files`指针。`mm`指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方;`files`指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。 +其中比较有意思的是 `mm` 指针和 `files` 指针。`mm` 指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方;`files` 指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。 ### 二、文件描述符是什么 -先说`files`,它是一个文件指针数组。一般来说,一个进程会从`files[0]`读取输入,将输出写入`files[1]`,将错误信息写入`files[2]`。 +先说 `files`,它是一个文件指针数组。一般来说,一个进程会从 `files[0]` 读取输入,将输出写入 `files[1]`,将错误信息写入 `files[2]`。 -举个例子,以我们的角度 C 语言的`printf`函数是向命令行打印字符,但是从进程的角度来看,就是向`files[1]`写入数据;同理,`scanf`函数就是进程试图从`files[0]`这个文件中读取数据。 +举个例子,以我们的角度 C 语言的 `printf` 函数是向命令行打印字符,但是从进程的角度来看,就是向 `files[1]` 写入数据;同理,`scanf` 函数就是进程试图从 `files[0]` 这个文件中读取数据。 -**每个进程被创建时,`files`的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引**,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。 +**每个进程被创建时,`files` 的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引**,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。 我们可以重新画一幅图: -![](../pictures/linuxProcess/2.jpg) +![](https://labuladong.online/algo/images/linuxProcess/2.jpg) 对于一般的计算机,输入流是键盘,输出流是显示器,错误流也是显示器,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的进程需要通过「系统调用」让内核进程访问硬件资源。 -PS:不要忘了,Linux 中一切都被抽象成文件,设备也是文件,可以进行读和写。 +> [!NOTE] +> 不要忘了,Linux 中一切都被抽象成文件,设备也是文件,可以进行读和写。 -如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到`files`的第 4 个位置: +如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到 `files` 的第 4 个位置: -![](../pictures/linuxProcess/3.jpg) +![](https://labuladong.online/algo/images/linuxProcess/3.jpg) -明白了这个原理,**输入重定向**就很好理解了,程序想读取数据的时候就会去`files[0]`读取,所以我们只要把`files[0]`指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘: +明白了这个原理,**输入重定向**就很好理解了,程序想读取数据的时候就会去 `files[0]` 读取,所以我们只要把 `files[0]` 指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘: ```shell $ command < file.txt ``` -![](../pictures/linuxProcess/5.jpg) +![](https://labuladong.online/algo/images/linuxProcess/5.jpg) -同理,**输出重定向**就是把`files[1]`指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中: +同理,**输出重定向**就是把 `files[1]` 指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中: ```shell $ command > file.txt ``` -![](../pictures/linuxProcess/4.jpg) +![](https://labuladong.online/algo/images/linuxProcess/4.jpg) 错误重定向也是一样的,就不再赘述。 @@ -87,9 +98,9 @@ $ command > file.txt $ cmd1 | cmd2 | cmd3 ``` -![](../pictures/linuxProcess/6.jpg) +![](https://labuladong.online/algo/images/linuxProcess/6.jpg) -到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的`files`数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美高效。 +到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的 `files` 数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美高效。 ### 三、线程是什么 @@ -97,13 +108,13 @@ $ cmd1 | cmd2 | cmd3 为什么说 Linux 中线程和进程基本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和进程区别对待。 -我们知道系统调用`fork()`可以新建一个子进程,函数`pthread()`可以新建一个线程。**但无论线程还是进程,都是用`task_struct`结构表示的,唯一的区别就是共享的数据区域不同**。 +我们知道系统调用 `fork()` 可以新建一个子进程,函数 `pthread()` 可以新建一个线程。**但无论线程还是进程,都是用 `task_struct` 结构表示的,唯一的区别就是共享的数据区域不同**。 -换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就比如说,`mm`结构和`files`结构在线程中都是共享的,我画两张图你就明白了: +换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就比如说,`mm` 结构和 `files` 结构在线程中都是共享的,我画两张图你就明白了: -![](../pictures/linuxProcess/7.jpg) +![](https://labuladong.online/algo/images/linuxProcess/7.jpg) -![](../pictures/linuxProcess/8.jpg) +![](https://labuladong.online/algo/images/linuxProcess/8.jpg) 所以说,我们的多线程程序要利用锁机制,避免多个线程同时往同一区域写入数据,否则可能造成数据错乱。 @@ -115,13 +126,23 @@ $ cmd1 | cmd2 | cmd3 在 Linux 中新建线程和进程的效率都是很高的,对于新建进程时内存区域拷贝的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父进程的内存空间,而是等到需要写操作时才去复制。**所以 Linux 中新建进程和新建线程都是很迅速的**。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +
+
+引用本文的文章 + + - [Linux 管道符的坑](https://labuladong.online/algo/fname.html?fname=linux技巧3) + - [关于 Linux shell 你必须知道的技巧](https://labuladong.online/algo/fname.html?fname=linuxshell) + +

+ + + + + +**_____________** -[上一篇:双指针技巧解题框架](../算法思维系列/双指针技巧.md) -[下一篇:Git/SQL/正则表达式的在线练习平台](../技术/在线练习平台.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" "b/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" index e971a88586..1e43837d05 100644 --- "a/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" +++ "b/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" @@ -1,10 +1,23 @@ +# Redis 入侵 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + 好吧,我也做了回标题党,像我这么细心的同学,怎么可能让服务器被入侵呢? 其实是这样的,昨天我和一个朋友聊天,他说他自己有一台云服务器运行了 Redis 数据库,有一天突然发现数据库里的**数据全没了**,只剩下一个奇奇怪怪的键值对,其中值看起来像一个 RSA 公钥的字符串,他以为是误操作删库了,幸好自己的服务器里没啥重要的数据,也就没在意。 经过一番攀谈交心了解到,他跑了一个比较古老已经停止维护的开源项目,安装的旧版本的 Redis,而且他对 Linux 的使用不是很熟练。我就知道,他的服务器已经被攻陷了,想到也许还会有不少像我这位朋友的人,不重视操作系统的权限、防火墙的设置和数据库的保护,我就写一篇文章简单看看这种情况出现的原因,以及如何防范。 -PS:这种手法现在已经行不通了,因为新版本 Redis 都增加了 protect mode,增加了安全性,我们只能在本地简单模拟一下,就别乱试了。 +> [!NOTE] +> 这种手法现在已经行不通了,因为新版本 Redis 都增加了 protect mode,增加了安全性,我们只能在本地简单模拟一下,就别乱试了。 ### 事件经过 @@ -30,29 +43,29 @@ Redis 监听的默认端口是 6379,我们设置它接收网卡 127.0.0.1 的 除了密码登录之外,还可以使用 RSA 密钥对登录,但是必须要把我的公钥存到 root 的家目录中 `/root/.ssh/authored_keys`。我们知道 `/root` 目录的权限设置是不允许任何其他用户闯入读写的: -![](../pictures/redis入侵/1.png) +![](https://labuladong.online/algo/images/redis/1.png) 但是,我发现自己竟然可以直接访问 Redis: -![](../pictures/redis入侵/2.png) +![](https://labuladong.online/algo/images/redis/2.png) 如果 Redis 是以 root 的身份运行的,那么我就可以通过操作 Redis,让它把我的公钥写到 root 的家目录中。Redis 有一种持久化方式是生成 RDB 文件,其中会包含原始数据。 我露出了邪恶的微笑,先把 Redis 中的数据全部清空,然后把我的 RSA 公钥写到数据库里,这里在开头和结尾加换行符目的是避免 RDB 文件生成过程中损坏到公钥字符串: -![](../pictures/redis入侵/3.png) +![](https://labuladong.online/algo/images/redis/3.png) 命令 Redis 把生成的数据文件保存到 `/root/.ssh/` 中的 `authored_keys` 文件中: -![](../pictures/redis入侵/4.png) +![](https://labuladong.online/algo/images/redis/4.png) 现在,root 的家目录中已经包含了我们的 RSA 公钥,我们现在可以通过密钥对登录进 root 了: -![](../pictures/redis入侵/5.png) +![](https://labuladong.online/algo/images/redis/5.png) 看一下刚才写入 root 家的公钥: -![](../pictures/redis入侵/6.png) +![](https://labuladong.online/algo/images/redis/6.png) 乱码是 GDB 文件的某种编码吧,但是中间的公钥被完整保存了,而且 ssh 登录程序竟然也识别了这段被乱码包围的公钥! @@ -78,6 +91,12 @@ Redis 监听的默认端口是 6379,我们设置它接收网卡 127.0.0.1 的 3、利用 rename 功能伪装 flushall 这种危险命令,以防被删库,丢失数据。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\212\200\346\234\257/session\345\222\214cookie.md" "b/\346\212\200\346\234\257/session\345\222\214cookie.md" index bc7b994e88..25ba2e78ff 100644 --- "a/\346\212\200\346\234\257/session\345\222\214cookie.md" +++ "b/\346\212\200\346\234\257/session\345\222\214cookie.md" @@ -1,3 +1,15 @@ +# 一文读懂 session 和 cookie + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + cookie 大家应该都熟悉,比如说登录某些网站一段时间后,就要求你重新登录;再比如有的同学很喜欢玩爬虫技术,有时候网站就是可以拦截住你的爬虫,这些都和 cookie 有关。如果你明白了服务器后端对于 cookie 和 session 的处理逻辑,就可以解释这些现象,甚至钻一些空子无限白嫖,待我慢慢道来。 ### 一、session 和 cookie 简介 @@ -29,11 +41,11 @@ func cookie(w http.ResponseWriter, r *http.Request) { 当浏览器访问对应网址时,通过浏览器的开发者工具查看此次 HTTP 通信的细节,可以看见服务器的回应发出了两次 `SetCookie` 命令: -![](../pictures/session/1.png) +![](https://labuladong.online/algo/images/session/1.png) 在这之后,浏览器的请求中的 `Cookie` 字段就带上了这两个 cookie: -![](../pictures/session/2.png) +![](https://labuladong.online/algo/images/session/2.png) **cookie 的作用其实就是这么简单,无非就是服务器给每个客户端(浏览器)打的标签**,方便服务器辨认而已。当然,HTTP 还有很多参数可以设置 cookie,比如过期时间,或者让某个 cookie 只有某个特定路径才能使用等等。 @@ -53,7 +65,7 @@ session 就可以配合 cookie 解决这一问题,比如说一个 cookie 存 那如果我不让浏览器发送 cookie,每次都伪装成一个第一次来试用的小萌新,不就可以不断白嫖了么?浏览器会把网站的 cookie 以文件的形式存在某些地方(不同的浏览器配置不同),你把他们找到然后删除就行了。但是对于 Firefox 和 Chrome 浏览器,有很多插件可以直接编辑 cookie,比如我的 Chrome 浏览器就用的一款叫做 EditThisCookie 的插件,这是他们官网: -![http://www.editthiscookie.com/](../pictures/session/3.png) +![](https://labuladong.online/algo/images/session/3.png) 这类插件可以读取浏览器在当前网页的 cookie,点开插件可以任意编辑和删除 cookie。**当然,偶尔白嫖一两次还行,不鼓励高频率白嫖,想常用还是掏钱吧,否则网站赚不到钱,就只能取消免费试用这个机制了**。 @@ -63,7 +75,7 @@ session 就可以配合 cookie 解决这一问题,比如说一个 cookie 存 session 的原理不难,但是具体实现它可是很有技巧的,一般需要三个组件配合完成,它们分别是 `Manager`、`Provider` 和 `Session` 三个类(接口)。 -![](../pictures/session/4.jpg) +![](https://labuladong.online/algo/images/session/4.jpg) 1、浏览器通过 HTTP 协议向服务器请求路径 `/content` 的网页资源,对应路径上有一个 Handler 函数接收请求,解析 HTTP header 中的 cookie,得到其中存储的 sessionID,然后把这个 ID 发给 `Manager`。 @@ -77,7 +89,6 @@ session 的原理不难,但是具体实现它可是很有技巧的,一般需 **这就是设计层面的技巧了**,下面就来说说,为什么分成 `Manager`、`Provider` 和 `Session`。 - 先从最底层的 `Session` 说。既然 session 就是键值对,为啥不直接用哈希表,而是要抽象出这么一个数据结构呢? 第一,因为 `Session` 结构可能不止存储了一个哈希表,还可以存储一些辅助数据,比如 `sid`,访问次数,过期时间或者最后一次的访问时间,这样便于实现想 LRU、LFU 这样的算法。 @@ -93,13 +104,14 @@ type Session interface { // 获取 key 对应的值 Get(key interface{}) interface{} // 删除键 key - Delete(key interface{}) + Delete(key interface{}) } ``` 再说 `Provider` 为啥要抽象出来。我们上面那个图的 `Provider` 就是一个散列表,保存 `sid` 到 `Session` 的映射,但是实际中肯定会更加复杂。我们不是要时不时删除一些 session 吗,除了设置存活时间之外,还可以采用一些其他策略,比如 LRU 缓存淘汰算法,这样就需要 `Provider` 内部使用哈希链表这种数据结构来存储 session。 -PS:关于 LRU 算法的奥妙,参见前文「LRU 算法详解」。 +> [!TIP] +> 关于 LRU 算法的奥妙,参见前文 [LRU 算法详解](https://labuladong.online/algo/data-structure/lru-cache/)。 因此,`Provider` 作为一个容器,就是要屏蔽算法细节,以合理的数据结构和算法组织 `sid` 和 `Session` 的映射关系,只需要实现下面这几个方法实现对 session 的增删查改: @@ -114,11 +126,10 @@ type Provider interface { // 修改一个session SessionUpdate(sid string) // 通过类似 LRU 的算法回收过期的 session - SessionGC(maxLifeTime int64) + SessionGC(maxLifeTime int64) } ``` - 最后说 `Manager`,大部分具体工作都委托给 `Session` 和 `Provider` 承担了,`Manager` 主要就是一个参数集合,比如 session 的存活时间,清理过期 session 的策略,以及 session 的可用存储方式。`Manager` 屏蔽了操作的具体细节,我们可以通过 `Manager` 灵活地配置 session 机制。 综上,session 机制分成几部分的最主要原因就是解耦,实现定制化。我在 Github 上看过几个 Go 语言实现的 session 服务,源码都很简单,有兴趣的朋友可以学习学习: @@ -127,13 +138,12 @@ https://github.com/alexedwards/scs https://github.com/astaxie/build-web-application-with-golang -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) -[下一篇:关于 Linux shell 你必须知道的](../技术/linuxshell.md) +**_____________** + + -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\212\200\346\234\257/\345\210\267\351\242\230\346\212\200\345\267\247.md" "b/\346\212\200\346\234\257/\345\210\267\351\242\230\346\212\200\345\267\247.md" new file mode 100644 index 0000000000..158aedf185 --- /dev/null +++ "b/\346\212\200\346\234\257/\345\210\267\351\242\230\346\212\200\345\267\247.md" @@ -0,0 +1,299 @@ +# 算法笔试「骗分」套路 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + + + +首先回答一个问题,刷力扣题是直接在网页上刷比较好还是在本地 IDE 上刷比较好? + +如果是牛客网笔试那种自己处理输入输出的判题形式,一定要在 IDE 上写,这个没啥说的,但**像力扣这种判题形式,我个人偏好直接在网页上刷**,原因有二: + +**1、方便** + +因为力扣有的数据结构是自定的,比如说 `TreeNode`,`ListNode` 这种,在本地你还得把这个类 copy 过去。 + +而且在 IDE 上没办法测试,写完代码之后还得粘贴到网页上跑测试数据,那还不如直接网页上写呢。 + +算法又不是工程代码,量都比较小,IDE 的自动补全带来的收益基本可以忽略不计。 + +**2、实用** + +到时候面试的时候,面试官给你出的算法题大都是希望你直接在网页上完成的,最好是边写边讲你的思路。 + +如果平时练习的时候就习惯没有 IDE 的自动补全,习惯手写代码大脑编译,到时候面试的时候写代码就能更快更从容。 + +之前我面快手的时候,有个面试官让我 [实现 LRU 算法](https://labuladong.online/algo/data-structure/lru-cache/),我直接把双链表的实现、哈希链表的实现,在网页上全写出来了,而且一次无 bug 跑通,可以看到面试官惊讶的表情😂 + +我秋招能当 offer 收割机,很大程度上就是因为手写算法这一关超出面试官的预期,其实都是因为之前在网页上刷题练出来的。 + +当然,实在不想在网页上刷,也可以用我的 vscode 刷题插件或者 JetBrains 刷题插件,插件和我的网站内容都有完美的融合。 + +接下来介绍几个很实用的「投机取巧」的办法和调试技巧,全方位提高你通过笔试的概率。 + + + + + + + +## 避实就虚 + +大家也知道,大部分笔试题目都需要你自己来处理输入数据,然后让程序打印输出。判题的底层原理是,把你程序的输出用 Linux 重定向符 `>` 写到文件里面,然后比较你的输出和正确答案是否相同。 + +那么有的问题难点就变得形同虚设,我们可以偷工减料,举个简化的例子,假设题目说给你输入一串用空格分隔的字符,告诉你这代表一个单链表,请你把这个单链表翻转,并且强调,一定要把输入的数字转化成单链表之后再翻转哦! + +那你怎么做?真就自己定义一个 `ListNode` 单链表节点类,然后再写代码把输入转化成一个单链表,然后再用让人头晕的指针操作去老老实实翻转单链表? + +搞清楚我们是来 AC 题目的,不是来学习算法思维的,判题系统是无法准确判断的算法逻辑的,只能判断你的输出是否正确。所以取巧的做法是直接把输入存到数组里,然后用 [双指针技巧](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) 几行代码给它翻转了,然后打印出来完事儿。 + +我就见过不少这种题目,比如题目说输入的是一个单链表,让我分组翻转链表,而且还特别强调要用递归实现,就是我们前文 [K 个一组翻转链表](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/) 的算法。嗯,如果用数组进行翻转,两分钟就写出来了,嘿嘿。 + +还有我们前文 [扁平化嵌套列表](https://labuladong.online/algo/data-structure/flatten-nested-list-iterator/) 讲到的题目,思路很巧妙,但是在笔试中遇到时,输入是一个形如 `[1,[4,[6]]]` 的字符串,那直接用正则表达式把数字抽出来,就是一个扁平化的列表了…… + + + + + + + +## 编程语言的选择 + +仅从做算法题的角度来说,我个人比较建议使用 Java 作为笔试的编程语言。因为 JetBrain 家的 IntelliJ 实在是太香了,相比其他语言的编辑器,不仅有 `psvm` 和 `sout` 这样的快捷命令(你要是连这都不知道,赶紧面壁去),而且可以帮你检查出很多笔误,比如说 `while` 循环里面忘记递增变量,或者 `return` 语句错写到循环里这种由于疏忽所导致的问题。 + +C++ 也还行,但是我觉得没有 Java 好用。我印象中 C++ 连个分割字符串的 `split` 函数都没有,光这点我就不想用 C++ 了…… + +还有一点,C++ 代码对时间的限制高,别的语言时间限制 4000ms,C++ 限制 2000ms,我觉得挺吃亏的。怪不得看别人用 C++ 写算法,为了提高速度,都不用标准库的 `vector` 容器,非要用原始的 `int[]` 数组,我看着都头疼。 + +Python 的话我刷题用的比较少,因为我不太喜欢用动态语言,不好调试。不过这个语言确实提供很多实用的功能,如果你深谙 Python 的套路,可以在某些时候投机取巧。比如说我们前文写到的 [表达式求值算法](https://labuladong.online/algo/data-structure/implement-calculator/) 是一个困难级别的算法,但如果用 Python 内置的 `exec` 函数,直接就能算出答案。 + +这个在笔试里肯定是很占便宜的,因为之前说了,我们要的是结果,没人在乎你是怎么得到结果的。 + + + + + + + +## 解法代码分层 + +代码分层应该算是一种比较好的习惯,可以增加写代码的速度和降低调试的难度。 + +简单说就是,不要把所有代码都写在 `main` 函数里面,我一直使用的套路是,`main` 函数负责接收数据,加一个 `solution` 函数负责统一处理数据和输出答案,然后再用诸如 `backtrack` 这样一个函数处理具体的算法逻辑。 + +举个例子,比如说一道题,我决定用带备忘录的动态规划求解,代码的大致结构是这样: + + + + + +```java +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + // 主要负责接收数据 + int N = scanner.nextInt(); + int[][] orders = new int[N][2]; + for (int i = 0; i < N; i++) { + orders[i][0] = scanner.nextInt(); + orders[i][1] = scanner.nextInt(); + } + // 委托 solution 进行求解 + solution(orders); + } + + static void solution(int[][] orders) { + // 排除一些基本的边界情况 + if (orders.length == 0) { + System.out.println("None"); + return; + } + // 委托 dp 函数执行具体的算法逻辑 + int res = dp(orders, 0); + // 负责输出结果 + System.out.println(res); + } + + // 备忘录 + static HashMap memo = new HashMap<>(); + static int dp(int[][] orders, int start) { + // 具体的算法逻辑 + } +} +``` + + + +你看这样分层是不是很清楚,每个函数都有自己主要负责的任务,如果哪里出了问题,你也容易 debug。 + +倒不是说要把代码写得多规范,至于 `private` 这种约束免了也无妨,变量用拼音命名也 OK,关键是别把代码直接全写到 `main` 函数里面,真的乱,不出错也罢,一旦出错,估计要花一番功夫调试了,找不到问题乱了阵脚,那是要尽量避免的。 + +## 如何给算法 debug + +代码的错误是无法避免的,有时候可能整个思路都错了,有时候可能是某些细节问题,比如 `i` 和 `j` 写反了,这种问题怎么排查? + +我想一般的算法问题肯定不难排查,肉眼检查应该都没啥问题,再不济 `print` 打印一些关键变量的值,总能发现问题。 + +**比较让人头疼的的应该是递归算法的问题排查**。 + +如果没有一定的经验,函数递归的过程很难被正确理解,所以这里就重点讲讲如何高效 debug 递归算法。 + +有的读者可能会说,把算法 copy 到 IDE 里面,然后打断点一步步跟着走不就行了吗? + +这个方法肯定是可以的,但是之前的文章多次说过,递归函数最好从一个全局的角度理解,而不要跳进具体的细节。 + +如果你对递归还不够熟悉,没有一个全局的视角,这种一步步打断点的方式也容易把人绕进去。 + +**我的建议是直接在递归函数内部打印关键值,配合缩进,直观地观察递归函数执行情况**。 + +最能提升我们 debug 效率的是缩进,除了解法函数,我们新定义一个函数 `printIndent` 和一个全局变量 `count`: + + + + + +```java +// 全局变量,记录递归函数的递归层数 +int count = 0; + +// 输入 n,打印 n 个 tab 缩进 +void printIndent(int n) { + for (int i = 0; i < n; i++) { + printf(" "); + } +} +``` + + + +接下来,套路来了: + +**在递归函数的开头,调用 `printIndent(count++)` 并打印关键变量;然后在所有 `return` 语句之前调用 `printIndent(--count)` 并打印返回值**。 + +举个具体的例子,比如说上篇文章 [辐射四游戏中的动态规划](https://labuladong.online/algo/dynamic-programming/freedom-trail/) 中实现了一个递归的 `dp` 函数,大致的结构如下: + + + + + +```java +int dp(String ring, int i, String key, int j) { + // base case + if (j == key.length()) { + return 0; + } + + // 状态转移 + for (int k : charToIndex.get(key.charAt(j))) { + int subProblem = dp(ring, k, key, j + 1); + } + + return res; +} +``` + + + +这个递归的 `dp` 函数在我进行了 debug 之后,变成了这样: + + + + + +```java +int count = 0; +void printIndent(int n) { + for (int i = 0; i < n; i++) { + System.out.print(" "); + } +} + +int dp(String ring, int i, String key, int j) { + // printIndent(count++); + // printf("i = %d, j = %d\n", i, j); + + if (j == key.length()) { + // printIndent(--count); + // printf("return 0\n"); + return 0; + } + + + for (int k : charToIndex.get(key.charAt(j))) { + int subProblem = dp(ring, k, key, j + 1); + } + + // printIndent(--count); + // printf("return %d\n", res); + return res; +} +``` + + + +**就是在函数开头和所有 `return` 语句对应的地方加上一些打印代码**。 + +如果去掉注释,执行一个测试用例,输出如下: + +![](https://labuladong.online/algo/images/algo-debug-tech/1.jpg) + +这样,我们通过对比对应的缩进就能知道每次递归时输入的关键参数 `i, j` 的值,以及每次递归调用返回的结果是多少。 + +**最重要的是,这样可以比较直观地看出递归过程,你有没有发现这就是一棵递归树**? + +![](https://labuladong.online/algo/images/algo-debug-tech/2.jpg) + +前文 [动态规划套路详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 说过,理解递归函数最重要的就是画出递归树,这样打印一下,连递归树都不用自己画了,而且还能清晰地看出每次递归的返回值。 + +**可以说,这是对刷题「幸福感」提升最大的一个小技巧,比 IDE 打断点要高效**。 + +我在可视化面板中直接支持了这个功能,在递归函数中打印的值会自动添加缩进,具体请看 [可视化面板使用说明](https://labuladong.online/algo/intro/visualize/)。 + + + + + + + +## 考前复习策略 + +考前就别和某一道算法题死磕了,不划算。 + +应该尽可能多的看各种各样的题目,思考五分钟,想不出来解法的话直接看别人的答案。看懂思路就行了,甚至自己写一遍都没必要,因为比较浪费时间。 + +笔试的时候最怕的是没思路,所以把各种题型都过目一下,起码心里不会慌,只要有思路,平均一道题二三十分钟搞定还是不难的。 + +前面不是说了么,没有什么问题是暴力穷举解决不了的,直接用 [回溯算法套路框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) 硬上,大不了加个备忘录,不就成 [动态规划套路框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 了么,再大不了这题我不做了么,暴力过上 60% 的 case 也挺 OK 的。 + +别的不多说了,套路这个东西,说来简单,一点就透,但问题是不点就不透。本文我简单介绍了几个笔试算法的技巧,各位好好品味~ + + + + + + + +
+
+引用本文的文章 + + - [带权重的随机选择算法](https://labuladong.online/algo/frequency-interview/random-pick-with-weight/) + +

+ + + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" "b/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" index f05434b0bd..871cdd2ff7 100644 --- "a/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" +++ "b/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" @@ -1,3 +1,15 @@ +# 在线刷题学习平台 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + 虽说我没事就喜欢喷应试教育,但我也从应试教育中发现了一个窍门:如果能够以刷题的形式学习某项技能,效率和效果是最佳的。对于技术的学习,我经常面临的困境是,**理论知识知道的不少,但是有的场景实在无法模拟,缺少亲自动手实践的机会**,如果能有一本带标准答案的习题册让我刷刷就好了。 所以在学习新技术时,我首先会去搜索是否有在线刷题平台,你还别说,有的大神真就做了很不错的在线练习平台,下面就介绍几个平台,分别是学习 Git、SQL、正则表达式的在线练习平台。 @@ -6,7 +18,7 @@ 这是个叫做 Learning Git Branching 的项目,是我一定要推荐的: -![](../pictures/online/1.png) +![](https://labuladong.online/algo/images/online/1.png) 正如对话框中的自我介绍,这确实也是我至今发现的**最好**的 Git 动画教程,没有之一。 @@ -14,21 +26,21 @@ 这个网站的教程不是给你举那种修改文件的细节例子,而是将每次 `commit` 都抽象成树的节点,**用动画闯关的形式**,让你自由使用 Git 命令完成目标: -![](../pictures/online/2.png) +![](https://labuladong.online/algo/images/online/2.png) 所有 Git 分支都被可视化了,你只要在左侧的命令行输入 Git 命令,分支会进行相应的变化,只要达成任务目标,你就过关啦!网站还会记录你的命令数,试试能不能以最少的命令数过关! -![](../pictures/online/3.png) +![](https://labuladong.online/algo/images/online/3.png) 我一开始以为这个教程只包含本地 Git 仓库的版本管理,**后来我惊奇地发现它还有远程仓库的操作教程**! -![](../pictures/online/4.png) +![](https://labuladong.online/algo/images/online/4.png) -![](../pictures/online/5.png) +![](https://labuladong.online/algo/images/online/5.png) 真的跟玩游戏一样,难度设计合理,流畅度很好,我一玩都停不下来了,几小时就打通了,哈哈哈! -![](../pictures/online/6.png) +![](https://labuladong.online/algo/images/online/6.png) 总之,这个教程很适合初学和进阶,如果你觉得自己对 Git 的掌握还不太好,用 Git 命令还是靠碰运气,就可以玩玩这个教程,相信能够让你更熟练地使用 Git。 @@ -48,13 +60,13 @@ https://learngitbranching.js.org 先说练习平台,叫做 RegexOne: -![](../pictures/online/9.png) +![](https://labuladong.online/algo/images/online/9.png) 前面有基本教程,后面有一些常见的正则表达式题目,比如判断邮箱、URL、电话号,或者抽取日志的关键信息等等。 只要写出符合要求的正则表达式,就可以进入下一个问题,关键是每道题还有标准答案,可以点击下面的 solution 按钮查看: -![](../pictures/online/10.png) +![](https://labuladong.online/algo/images/online/10.png) RegexOne 网址: @@ -62,7 +74,7 @@ https://regexone.com/ 再说测试工具,是个叫做 RegExr 的 Github 项目,这是它的网站: -![](../pictures/online/11.png) +![](https://labuladong.online/algo/images/online/11.png) 可以看见,输入文本和正则模式串后,**网站会给正则表达式添加好看且容易辨认的样式,自动在文本中搜索模式串,高亮显示匹配的字符串,并且还会显示每个分组捕获的字符串**。 @@ -76,13 +88,13 @@ https://regexr.com/ 这是一个叫做 SQLZOO 的网站,左侧是所有的练习内容: -![](../pictures/online/7.png) +![](https://labuladong.online/algo/images/online/7.png) SQLZOO 是一款很好用的 SQL 练习平台,英文不难理解,可以直接看英文版,但是也可以切换繁体中文,比较友好。 这里都是比较常用的 SQL 命令,给你一个需求,你写 SQL 语句实现正确的查询结果。**最重要的是,这里不仅对每个命令的用法有详细解释,每个专题后面还有选择题(quiz),而且有判题系统,甚至有的比较难的题目还有视频讲解**: -![](../pictures/online/8.png) +![](https://labuladong.online/algo/images/online/8.png) 至于难度,循序渐进,即便对新手也很友好,靠后的问题确实比较有技巧性,相信这是热爱思维挑战的人喜欢的!LeetCode 也有 SQL 相关的题目,不过难度一般比较大,我觉得 SQLZOO 刷完基础 SQL 命令再去 LeetCode 刷比较合适。 @@ -91,13 +103,21 @@ SQLZOO 是一款很好用的 SQL 练习平台,英文不难理解,可以直 https://sqlzoo.net/ -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +
+
+引用本文的文章 + + - [四个基本命令搞会 Git 使用](https://labuladong.online/algo/fname.html?fname=git常用命令) + +

+ + + + +**_____________** -[上一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) -[下一篇:动态规划详解](../动态规划系列/动态规划详解进阶.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" "b/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" index 6203408d24..a795dede86 100644 --- "a/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" +++ "b/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" @@ -1,3 +1,15 @@ +# 密码算法的前世今生 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + 说到密码,我们第一个想到的就是登陆账户的密码,但是从密码学的角度来看,这种根本就不算合格的密码。 为什么呢,因为我们的账户密码,是依靠隐蔽性来达到加密作用:密码藏在我心里,你不知道,所以你登不上我的账户。 @@ -42,19 +54,19 @@ Diffie-Hellman 密钥交换算法可以做到。**准确的说,该算法并不 首先,Alice 和 Bob 协商出两个数字 `N` 和 `G` 作为生成元,当然协商过程可以被窃听者 Hack 窃取,所以我把这两个数画到中间,代表三方都知道: -![](../pictures/密码技术/1.jpg) +![](https://labuladong.online/algo/images/cryptography/1.jpg) 现在 Alice 和 Bob **心中**各自想一个数字出来,分别称为 `A` 和 `B` 吧: -![](../pictures/密码技术/2.jpg) +![](https://labuladong.online/algo/images/cryptography/2.jpg) 现在 Alice 将自己心里的这个数字 `A` 和 `G` 通过某些运算得出一个数 `AG`,然后发给 Bob;Bob 将自己心里的数 `B` 和 `G` 通过相同的运算得出一个数 `BG`,然后发给 Alice: -![](../pictures/密码技术/3.jpg) +![](https://labuladong.online/algo/images/cryptography/3.jpg) 现在的情况变成这样了: -![](../pictures/密码技术/4.jpg) +![](https://labuladong.online/algo/images/cryptography/4.jpg) 注意,类似刚才举的散列函数的例子,知道 `AG` 和 `G`,并不能反推出 `A` 是多少,`BG` 同理。 @@ -62,7 +74,7 @@ Diffie-Hellman 密钥交换算法可以做到。**准确的说,该算法并不 而对于 Hack,可以窃取传输过程中的 `G`,`AG`,`BG`,但是由于计算不可逆,怎么都无法结合出 `ABG` 这个数字。 -![](../pictures/密码技术/5.jpg) +![](https://labuladong.online/algo/images/cryptography/5.jpg) 以上就是基本流程,至于具体的数字取值是有讲究的,运算方法在百度上很容易找到,限于篇幅我就不具体写了。 @@ -70,7 +82,7 @@ Diffie-Hellman 密钥交换算法可以做到。**准确的说,该算法并不 对于该算法,Hack 又想到一种破解方法,不是窃听 Alice 和 Bob 的通信数据,而是直接同时冒充 Alice 和 Bob 的身份,也就是我们说的「**中间人攻击**」: -![](../pictures/密码技术/6.jpg) +![](https://labuladong.online/algo/images/cryptography/6.jpg) 这样,双方根本无法察觉在和 Hack 共享秘密,后果就是 Hack 可以解密甚至修改数据。 @@ -142,9 +154,10 @@ Diffie-Hellman 密钥交换算法可以做到。**准确的说,该算法并不 4、Alice 通过这个公钥加密数据,开始和 Bob 通信。 -![图片来自《图解密码技术》](../pictures/密码技术/7.jpg) +![](https://labuladong.online/algo/images/cryptography/7.jpg) -PS:以上只是为了说明,证书只需要安装一次,并不需要每次都向认证机构请求;一般是服务器直接给客户端发送证书,而不是认证机构。 +> [!NOTE] +> 以上只是为了说明,证书只需要安装一次,并不需要每次都向认证机构请求;一般是服务器直接给客户端发送证书,而不是认证机构。 也许有人问,Alice 要想通过数字签名确定证书的有效性,前提是要有该机构的(认证)公钥,这不是又回到刚才的死循环了吗? @@ -174,13 +187,12 @@ HTTPS 协议中的 SSL/TLS 安全层会组合使用以上几种加密方式,** 密码技术只是安全的一小部分,即便是通过正规机构认证的 HTTPS 站点,也不意味着可信任,只能说明其数据传输是安全的。技术永远不可能真正保护你,最重要的还是得提高个人的安全防范意识,多留心眼儿,谨慎处理敏感数据。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:关于 Linux shell 你必须知道的](../技术/linuxshell.md) -[下一篇:Git/SQL/正则表达式的在线练习平台](../技术/在线练习平台.md) +**_____________** + + -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST1.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST1.md" new file mode 100644 index 0000000000..b40e5374a9 --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST1.md" @@ -0,0 +1,280 @@ +# 二叉搜索树心法(特性篇) + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1038. Binary Search Tree to Greater Sum Tree](https://leetcode.com/problems/binary-search-tree-to-greater-sum-tree/) | [1038. 从二叉搜索树到更大和树](https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree/) | 🟠 | +| [230. Kth Smallest Element in a BST](https://leetcode.com/problems/kth-smallest-element-in-a-bst/) | [230. 二叉搜索树中第K小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/) | 🟠 | +| [538. Convert BST to Greater Tree](https://leetcode.com/problems/convert-bst-to-greater-tree/) | [538. 把二叉搜索树转换为累加树](https://leetcode.cn/problems/convert-bst-to-greater-tree/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树结构基础](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) +> - [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) + +前文手把手带你刷二叉树已经写了 [思维篇](https://labuladong.online/algo/data-structure/binary-tree-part1/),[构造篇](https://labuladong.online/algo/data-structure/binary-tree-part2/),[后序篇](https://labuladong.online/algo/data-structure/binary-tree-part3/) 和 [序列化篇](https://labuladong.online/algo/data-structure/serialize-and-deserialize-binary-tree/)。 + +今天开启二叉搜索树(Binary Search Tree,后文简写 BST)的系列文章,手把手带你刷 BST。 + +首先,BST 的特性大家应该都很熟悉了(详见基础知识章节的 [二叉树基础](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/)): + +1、对于 BST 的每一个节点 `node`,左子树节点的值都比 `node` 的值要小,右子树节点的值都比 `node` 的值大。 + +2、对于 BST 的每一个节点 `node`,它的左侧子树和右侧子树都是 BST。 + +二叉搜索树并不算复杂,但我觉得它可以算是数据结构领域的半壁江山,直接基于 BST 的数据结构有 AVL 树,红黑树等等,拥有了自平衡性质,可以提供 logN 级别的增删查改效率;还有 B+ 树,线段树等结构都是基于 BST 的思想来设计的。 + +**从做算法题的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)**。 + +也就是说,如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来: + +```java +void traverse(TreeNode root) { + if (root == null) return; + traverse(root.left); + // 中序遍历代码位置 + print(root.val); + traverse(root.right); +} +``` + +那么根据这个性质,我们来做两道算法题。 + +## 寻找第 K 小的元素 + +这是力扣第 230 题「二叉搜索树中第 K 小的元素」,看下题目: + + + +这个需求很常见吧,一个直接的思路就是升序排序,然后找第 `k` 个元素呗。BST 的中序遍历其实就是升序排序的结果,找第 `k` 个元素肯定不是什么难事。 + +按照这个思路,可以直接写出代码: + +```java +class Solution { + int kthSmallest(TreeNode root, int k) { + // 利用 BST 的中序遍历特性 + traverse(root, k); + return res; + } + + // 记录结果 + int res = 0; + // 记录当前元素的排名 + int rank = 0; + void traverse(TreeNode root, int k) { + if (root == null) { + return; + } + traverse(root.left, k); + + // 中序代码位置 + rank++; + if (k == rank) { + // 找到第 k 小的元素 + res = root.val; + return; + } + + traverse(root.right, k); + } +} +``` + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +这道题就做完了,不过呢,还是要多说几句,因为这个解法并不是最高效的解法,而是仅仅适用于这道题。 + +我们前文 [高效计算数据流的中位数](https://labuladong.online/algo/practice-in-action/find-median-from-data-stream/) 中就提过今天的这个问题: + +> [!NOTE] +> 如果让你实现一个在二叉搜索树中通过排名计算对应元素的方法 `select(int k)`,你会怎么设计? + +如果按照我们刚才说的方法,利用「BST 中序遍历就是升序排序结果」这个性质,每次寻找第 `k` 小的元素都要中序遍历一次,最坏的时间复杂度是 $O(N)$,`N` 是 BST 的节点个数。 + +要知道 BST 性质是非常牛逼的,像红黑树这种改良的自平衡 BST,增删查改都是 $O(logN)$ 的复杂度,让你算一个第 `k` 小元素,时间复杂度竟然要 $O(N)$,有点低效了。 + +所以说,计算第 `k` 小元素,最好的算法肯定也是对数级别的复杂度,不过这个依赖于 BST 节点记录的信息有多少。 + +我们想一下 BST 的操作为什么这么高效?就拿搜索某一个元素来说,BST 能够在对数时间找到该元素的根本原因还是在 BST 的定义里,左子树小右子树大嘛,所以每个节点都可以通过对比自身的值判断去左子树还是右子树搜索目标值,从而避免了全树遍历,达到对数级复杂度。 + +那么回到这个问题,想找到第 `k` 小的元素,或者说找到排名为 `k` 的元素,如果想达到对数级复杂度,关键也在于每个节点得知道他自己排第几。 + +比如说你让我查找排名为 `k` 的元素,当前节点知道自己排名第 `m`,那么我可以比较 `m` 和 `k` 的大小: + +1、如果 `m == k`,显然就是找到了第 `k` 个元素,返回当前节点就行了。 + +2、如果 `k < m`,那说明排名第 `k` 的元素在左子树,所以可以去左子树搜索第 `k` 个元素。 + +3、如果 `k > m`,那说明排名第 `k` 的元素在右子树,所以可以去右子树搜索第 `k - m - 1` 个元素。 + +这样就可以将时间复杂度降到 $O(logN)$ 了。 + +那么,如何让每一个节点知道自己的排名呢? + +这就是我们之前说的,需要在二叉树节点中维护额外信息。**每个节点需要记录,以自己为根的这棵二叉树有多少个节点**。 + +也就是说,我们 `TreeNode` 中的字段应该如下: + +```java +class TreeNode { + int val; + // 以该节点为根的树的节点总数 + int size; + TreeNode left; + TreeNode right; +} +``` + +有了 `size` 字段,外加 BST 节点左小右大的性质,对于每个节点 `node` 就可以通过 `node.left` 推导出 `node` 的排名,从而做到我们刚才说到的对数级算法。 + +当然,`size` 字段需要在增删元素的时候需要被正确维护,力扣提供的 `TreeNode` 是没有 `size` 这个字段的,所以我们这道题就只能利用 BST 中序遍历的特性实现了,但是我们上面说到的优化思路是 BST 的常见操作,还是有必要理解的。 + +## BST 转化累加树 + +力扣第 538 题和 1038 题都是这道题,完全一样,你可以把它们一块做掉。看下题目: + + + +题目应该不难理解,比如图中的节点 5,转化成累加树的话,比 5 大的节点有 6,7,8,加上 5 本身,所以累加树上这个节点的值应该是 5+6+7+8=26。 + +我们需要把 BST 转化成累加树,函数签名如下: + +```java +TreeNode convertBST(TreeNode root) +``` + +按照二叉树的通用思路,需要思考每个节点应该做什么,但是这道题上很难想到什么思路。 + +BST 的每个节点左小右大,这似乎是一个有用的信息,既然累加和是计算大于等于当前值的所有元素之和,那么每个节点都去计算右子树的和,不就行了吗? + +这是不行的。对于一个节点来说,确实右子树都是比它大的元素,但问题是它的父节点也可能是比它大的元素呀?这个没法确定的,我们又没有触达父节点的指针,所以二叉树的通用思路在这里用不了。 + +**此路不通,我们不妨换一个思路,还是利用 BST 的中序遍历特性**。 + +刚才我们说了 BST 的中序遍历代码可以升序打印节点的值,那如果我想降序打印节点的值怎么办? + +很简单,只要把递归顺序改一下,先遍历右子树,后遍历左子树就行了: + +```java +void traverse(TreeNode root) { + if (root == null) return; + // 先递归遍历右子树 + traverse(root.right); + // 中序遍历代码位置 + print(root.val); + // 后递归遍历左子树 + traverse(root.left); +} +``` + +**这段代码可以降序打印 BST 节点的值,如果维护一个外部累加变量 `sum`,然后把 `sum` 赋值给 BST 中的每一个节点,不就将 BST 转化成累加树了吗**? + +看下代码就明白了: + +```java +class Solution { + TreeNode convertBST(TreeNode root) { + traverse(root); + return root; + } + + // 记录累加和 + int sum = 0; + void traverse(TreeNode root) { + if (root == null) { + return; + } + traverse(root.right); + // 维护累加和 + sum += root.val; + // 将 BST 转化成累加树 + root.val = sum; + traverse(root.left); + } +} +``` + + +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
+ + + +这道题就解决了,核心还是 BST 的中序遍历特性,只不过我们修改了递归顺序,降序遍历 BST 的元素值,从而契合题目累加树的要求。 + +简单总结下吧,BST 相关的问题,要么利用 BST 左小右大的特性提升算法效率,要么利用中序遍历的特性满足题目的要求,也就这么些事儿吧。 + +本文就到这里,更多经典的二叉树习题以及递归思维的训练,请参见二叉树章节中的 [习题部分](https://labuladong.online/algo/problem-set/bst1/) + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】二叉搜索树经典例题 I](https://labuladong.online/algo/problem-set/bst1/) + - [二叉搜索树心法(基操篇)](https://labuladong.online/algo/data-structure/bst-part2/) + - [二叉搜索树心法(构造篇)](https://labuladong.online/algo/data-structure/bst-part3/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer II 054. 所有大于等于节点的值之和](https://leetcode.cn/problems/w6cpku/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST2.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST2.md" new file mode 100644 index 0000000000..d85dc16dd0 --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST2.md" @@ -0,0 +1,439 @@ +# 二叉搜索树心法(基操篇) + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [450. Delete Node in a BST](https://leetcode.com/problems/delete-node-in-a-bst/) | [450. 删除二叉搜索树中的节点](https://leetcode.cn/problems/delete-node-in-a-bst/) | 🟠 | +| [700. Search in a Binary Search Tree](https://leetcode.com/problems/search-in-a-binary-search-tree/) | [700. 二叉搜索树中的搜索](https://leetcode.cn/problems/search-in-a-binary-search-tree/) | 🟢 | +| [701. Insert into a Binary Search Tree](https://leetcode.com/problems/insert-into-a-binary-search-tree/) | [701. 二叉搜索树中的插入操作](https://leetcode.cn/problems/insert-into-a-binary-search-tree/) | 🟠 | +| [98. Validate Binary Search Tree](https://leetcode.com/problems/validate-binary-search-tree/) | [98. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树结构基础](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) +> - [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) + +我们前文 [二叉搜索树心法(特性篇)](https://labuladong.online/algo/data-structure/bst-part1/) 介绍了 BST 的基本特性,还利用二叉搜索树「中序遍历有序」的特性来解决了几道题目,本文来实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中「删」和「判断合法性」略微复杂。 + +BST 的基础操作主要依赖「左小右大」的特性,可以在二叉树中做类似二分搜索的操作,寻找一个元素的效率很高。比如下面这就是一棵合法的二叉树: + +![](https://labuladong.online/algo/images/bst/0.png) + +对于 BST 相关的问题,你可能会经常看到类似下面这样的代码逻辑: + +```java +void BST(TreeNode root, int target) { + if (root.val == target) + // 找到目标,做点什么 + if (root.val < target) + BST(root.right, target); + if (root.val > target) + BST(root.left, target); +} +``` + +这个代码框架其实和二叉树的遍历框架差不多,无非就是利用了 BST 左小右大的特性而已。接下来看下 BST 这种结构的基础操作是如何实现的。 + +## 一、判断 BST 的合法性 + +力扣第 98 题「验证二叉搜索树」就是让你判断输入的 BST 是否合法: + + + +注意,这里是有坑的哦。按照 BST 左小右大的特性,每个节点想要判断自己是否是合法的 BST 节点,要做的事不就是比较自己和左右孩子吗?感觉应该这样写代码: + +```java +boolean isValidBST(TreeNode root) { + if (root == null) return true; + // root 的左边应该更小 + if (root.left != null && root.left.val >= root.val) + return false; + // root 的右边应该更大 + if (root.right != null && root.right.val <= root.val) + return false; + + return isValidBST(root.left) + && isValidBST(root.right); +} +``` + +但是这个算法出现了错误,BST 的每个节点应该要小于右边子树的**所有**节点,下面这个二叉树显然不是 BST,因为节点 10 的右子树中有一个节点 6,但是我们的算法会把它判定为合法 BST: + +![](https://labuladong.online/algo/images/bst/假BST.png) + +**错误的原因在于,对于每一个节点 `root`,代码值检查了它的左右孩子节点是否符合左小右大的原则;但是根据 BST 的定义,`root` 的整个左子树都要小于 `root.val`,整个右子树都要大于 `root.val`**。 + +问题是,对于某一个节点 `root`,他只能管得了自己的左右子节点,怎么把 `root` 的约束传递给左右子树呢?请看正确的代码: + +```java +class Solution { + public boolean isValidBST(TreeNode root) { + return _isValidBST(root, null, null); + } + + // 定义:该函数返回 root 为根的子树的所有节点是否满足 max.val > root.val > min.val + public boolean _isValidBST(TreeNode root, TreeNode min, TreeNode max) { + // base case + if (root == null) return true; + // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST + if (min != null && root.val <= min.val) return false; + if (max != null && root.val >= max.val) return false; + // 根据定义,限定左子树的最大值是 root.val,右子树的最小值是 root.val + return _isValidBST(root.left, min, root) + && _isValidBST(root.right, root, max); + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +我们通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉树算法的一个小技巧吧。 + +## 在 BST 中搜索元素 + +力扣第 700 题「二叉搜索树中的搜索」就是让你在 BST 中搜索值为 `target` 的节点,函数签名如下: + +```java +TreeNode searchBST(TreeNode root, int target); +``` + +如果是在一棵普通的二叉树中寻找,可以这样写代码: + +```java +TreeNode searchBST(TreeNode root, int target) { + if (root == null) return null; + if (root.val == target) return root; + // 当前节点没找到就递归地去左右子树寻找 + TreeNode left = searchBST(root.left, target); + TreeNode right = searchBST(root.right, target); + + return left != null ? left : right; +} +``` + +这样写完全正确,但这段代码相当于穷举了所有节点,适用于所有二叉树。那么应该如何充分利用 BST 的特殊性,把「左小右大」的特性用上? + +很简单,其实不需要递归地搜索两边,类似二分查找思想,根据 `target` 和 `root.val` 的大小比较,就能排除一边。我们把上面的思路稍稍改动: + +```java +TreeNode searchBST(TreeNode root, int target) { + if (root == null) { + return null; + } + // 去左子树搜索 + if (root.val > target) { + return searchBST(root.left, target); + } + // 去右子树搜索 + if (root.val < target) { + return searchBST(root.right, target); + } + // 当前节点就是目标值 + return root; +} +``` + + +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +## 在 BST 中插入一个数 + +对数据结构的操作无非遍历 + 访问,遍历就是「找」,访问就是「改」。具体到这个问题,插入一个数,就是先找到插入位置,然后进行插入操作。 + +因为 BST 一般不会存在值重复的节点,所以我们一般不会在 BST 中插入已存在的值。**下面的代码都默认不会向 BST 中插入已存在的值**。 + +上一个问题,我们总结了 BST 中的遍历框架,就是「找」的问题。直接套框架,加上「改」的操作即可。 + +**一旦涉及「改」,就类似二叉树的构造问题,函数要返回 `TreeNode` 类型,并且要对递归调用的返回值进行接收**。 + +力扣第 701 题「二叉搜索树中的插入操作」就是这个问题: + + + +直接看解法代码吧,可以结合注释和可视化面板的来理解: + +```java +class Solution { + public TreeNode insertIntoBST(TreeNode root, int val) { + if (root == null) { + // 找到空位置插入新节点 + return new TreeNode(val); + } + + // 去右子树找插入位置 + if (root.val < val) { + root.right = insertIntoBST(root.right, val); + } + // 去左子树找插入位置 + if (root.val > val) { + root.left = insertIntoBST(root.left, val); + } + // 返回 root,上层递归会接收返回值作为子节点 + return root; + } +} +``` + + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + +## 三、在 BST 中删除一个数 + +力扣第 450 题「删除二叉搜索树中的节点」就是让你在 BST 中删除一个值为 `key` 的节点: + + + +这个问题稍微复杂,跟插入操作类似,先「找」再「改」,先把框架写出来再说: + +```java +TreeNode deleteNode(TreeNode root, int key) { + if (root.val == key) { + // 找到啦,进行删除 + } else if (root.val > key) { + // 去左子树找 + root.left = deleteNode(root.left, key); + } else if (root.val < key) { + // 去右子树找 + root.right = deleteNode(root.right, key); + } + return root; +} +``` + +找到目标节点了,比方说是节点 `A`,如何删除这个节点,这是难点。因为删除节点的同时不能破坏 BST 的性质。有三种情况,用图片来说明。 + +**情况 1**:`A` 恰好是末端节点,两个子节点都为空,那么它可以当场去世了。 + + + + + +![](https://labuladong.online/algo/images/bst/bst_deletion_case_1.png) + +```java +if (root.left == null && root.right == null) + return null; +``` + + + +**情况 2**:`A` 只有一个非空子节点,那么它要让这个孩子接替自己的位置。 + + + + + +![](https://labuladong.online/algo/images/bst/bst_deletion_case_2.png) + +```java +// 排除了情况 1 之后 +if (root.left == null) return root.right; +if (root.right == null) return root.left; +``` + + + +**情况 3**:`A` 有两个子节点,麻烦了,为了不破坏 BST 的性质,`A` 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。 + + + + + +![](https://labuladong.online/algo/images/bst/bst_deletion_case_3.png) + +```java +if (root.left != null && root.right != null) { + // 找到右子树的最小节点 + TreeNode minNode = getMin(root.right); + // 把 root 改成 minNode + root.val = minNode.val; + // 转而去删除 minNode + root.right = deleteNode(root.right, minNode.val); +} +``` + + + +三种情况分析完毕,填入框架,简化一下代码: + +```java +class Solution { + public TreeNode deleteNode(TreeNode root, int key) { + if (root == null) return null; + if (root.val == key) { + // 这两个 if 把情况 1 和 2 都正确处理了 + if (root.left == null) return root.right; + if (root.right == null) return root.left; + // 处理情况 3 + // 获得右子树最小的节点 + TreeNode minNode = getMin(root.right); + // 删除右子树最小的节点 + root.right = deleteNode(root.right, minNode.val); + // 用右子树最小的节点替换 root 节点 + minNode.left = root.left; + minNode.right = root.right; + root = minNode; + } else if (root.val > key) { + root.left = deleteNode(root.left, key); + } else if (root.val < key) { + root.right = deleteNode(root.right, key); + } + return root; + } + + TreeNode getMin(TreeNode node) { + // BST 最左边的就是最小的 + while (node.left != null) node = node.left; + return node; + } +} +``` + + +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
+ + + +这样,删除操作就完成了。注意一下,上述代码在处理情况 3 时通过一系列略微复杂的链表操作交换 `root` 和 `minNode` 两个节点: + + + + + +```java +// 处理情况 3 +// 获得右子树最小的节点 +TreeNode minNode = getMin(root.right); +// 删除右子树最小的节点 +root.right = deleteNode(root.right, minNode.val); +// 用右子树最小的节点替换 root 节点 +minNode.left = root.left; +minNode.right = root.right; +root = minNode; +``` + + + +有的读者可能会疑惑,替换 `root` 节点为什么这么麻烦,直接改 `val` 字段不就行了?看起来还更简洁易懂: + + + + + +```java +// 处理情况 3 +// 获得右子树最小的节点 +TreeNode minNode = getMin(root.right); +// 删除右子树最小的节点 +root.right = deleteNode(root.right, minNode.val); +// 用右子树最小的节点替换 root 节点 +root.val = minNode.val; +``` + + + +仅对于这道算法题来说是可以的,但这样操作并不完美,我们一般不会通过修改节点内部的值来交换节点。因为在实际应用中,BST 节点内部的数据域是用户自定义的,可以非常复杂,而 BST 作为数据结构(一个工具人),其操作应该和内部存储的数据域解耦,所以我们更倾向于使用指针操作来交换节点,根本没必要关心内部数据。 + +最后简单总结一下吧,通过这篇文章,我们总结出了如下几个技巧: + +1、如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。 + +2、掌握 BST 的增删查改方法。 + +3、递归修改数据结构时,需要对递归调用的返回值进行接收,并返回修改后的节点。 + +本文就到这里,更多经典的二叉树习题以及递归思维的训练,请参见二叉树章节中的 [递归专项练习](https://labuladong.online/algo/problem-set/bst1/) + + + + + + + +
+
+引用本文的文章 + + - [Trie/字典树/前缀树代码实现](https://labuladong.online/algo/data-structure/trie-implement/) + - [【强化练习】二叉搜索树经典例题 II](https://labuladong.online/algo/problem-set/bst2/) + - [二叉搜索树心法(后序篇)](https://labuladong.online/algo/data-structure/bst-part4/) + - [二叉搜索树心法(构造篇)](https://labuladong.online/algo/data-structure/bst-part3/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer 33. 二叉搜索树的后序遍历序列](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" index 736832579e..4b64bc4743 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" @@ -2,6 +2,6 @@ 这一章主要是一些特殊的数据结构设计,比如单调栈解决 Next Greater Number,单调队列解决滑动窗口问题;还有常用数据结构的操作,比如链表、树、二叉堆。 -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/dijkstra\347\256\227\346\263\225.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/dijkstra\347\256\227\346\263\225.md" new file mode 100644 index 0000000000..a870b92013 --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/dijkstra\347\256\227\346\263\225.md" @@ -0,0 +1,412 @@ +# Dijkstra 算法模板及应用 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [图结构基础及通用实现](https://labuladong.online/algo/data-structure-basic/graph-basic/) +> - [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) +> - [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) + +> [!IMPORTANT] +> Dijkstra 算法是一种用于计算图中单源最短路径的算法,本质上是一个经过特殊改造的 BFS 算法,改造点有两个: +> +> 1、使用 [优先级队列](https://labuladong.online/algo/data-structure-basic/binary-heap-implement/),而不是普通队列进行 BFS 算法。 +> +> 2、添加了一个备忘录,记录起点到每个可达节点的最短路径权重和。 + +学习 Dijkstra 最短路径算法之前,你需要先了解 [图结构基础及通用代码实现](https://labuladong.online/algo/data-structure-basic/graph-basic/),下面的讲解中,我会用到图结构 `Graph` 的通用 API。 + +另外,你必须要理解 [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) 以及 [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) 中 BFS 遍历的基本原理,因为 Dijkstra 算法本质上就是一个经过特殊改造的 BFS 算法。 + +在讲解二叉树和图结构的 BFS 遍历算法时,我同时给出了三种 BFS 算法的写法,如果忘了可以回去复习一下。 + +其中第三种 BFS 算法相对复杂一些,但是最灵活,因为它新建了一个 `State` 类,允许每个节点独立维护一些额外信息。 + +具体代码如下: + +```java +// 多叉树的层序遍历 +// 每个节点自行维护 State 类,记录深度等信息 +class State { + Node node; + int depth; + + public State(Node node, int depth) { + this.node = node; + this.depth = depth; + } +} + +void levelOrderTraverse(Node root) { + if (root == null) { + return; + } + Queue q = new LinkedList<>(); + // 记录当前遍历到的层数(根节点视为第 1 层) + q.offer(new State(root, 1)); + + while (!q.isEmpty()) { + State state = q.poll(); + Node cur = state.node; + int depth = state.depth; + // 访问 cur 节点,同时知道它所在的层数 + System.out.println("depth = " + depth + ", val = " + cur.val); + + for (Node child : cur.children) { + q.offer(new State(child, depth + 1)); + } + } +} + + +// 图结构的 BFS 遍历,从节点 s 开始进行 BFS,且记录路径的权重和 +// 每个节点自行维护 State 类,记录从 s 走来的权重和 +class State { + // 当前节点 ID + int node; + // 从起点 s 到当前节点的权重和 + int weight; + + public State(int node, int weight) { + this.node = node; + this.weight = weight; + } +} + + +void bfs(Graph graph, int s) { + boolean[] visited = new boolean[graph.size()]; + Queue q = new LinkedList<>(); + + q.offer(new State(s, 0)); + visited[s] = true; + + while (!q.isEmpty()) { + State state = q.poll(); + int cur = state.node; + int weight = state.weight; + System.out.println("visit " + cur + " with path weight " + weight); + for (Edge e : graph.neighbors(cur)) { + if (!visited[e.to]) { + q.offer(new State(e.to, weight + e.weight)); + visited[e.to] = true; + } + } + } +} +``` + +这种写法对于树结构来说有些多此一举,但是对于加权图来说,就非常有用了。 + + + +在这个可视化面板中,我创建了一幅加权图。你可以多次点击 console.log 这一行代码,注意命令行的输出,这种写法可以在遍历节点的同时得知起点到当前节点的路径和: + + + +我们即将实现的 Dijkstra 算法就是基于这个算法的改进,每个节点都需要记录从起点到自己的最短路径权重和,再结合 [优先级队列](https://labuladong.online/algo/data-structure-basic/binary-heap-implement/) 这种能够动态排序的数据结构,就可以高效地计算出最短路径了。 + +下面来具体介绍 Dijkstra 算法的通用代码实现。 + + + + + + + +## Dijkstra 函数签名 + +首先,我们可以写一个 Dijkstra 算法的通用函数签名: + +```java +// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离 +int[] dijkstra(int start, Graph graph); +``` + +输入是一幅图 `graph` 和一个起点 `start`,返回是一个记录最短路径权重的数组,比方下面这个例子: + + + + + +```java +int[] distTo = dijkstra(3, graph); +``` + + + +`distTo` 数组中存储节点 `3` 作为起点到其他节点的最小路径和,比如从起点 `3` 到节点 `6` 的最短路径权重和就是 `distTo[6]`。 + +因为是本质上就是 BFS 嘛,所以标准的 Dijkstra 算法会从起点 `start` 开始遍历,把到所有其他可达节点的最短路径都算出来。 + +当然,如果你的需求只是计算从起点 `start` 到某一个终点 `end` 的最短路径,那么在标准 Dijkstra 算法上稍作修改就可以更高效地完成这个需求,这个我们后面再说。 + +## `State` 类 + +我们也需要一个 `State` 类来辅助 BFS 算法的运行,清晰起见,我们用 `id` 变量记录当前节点 ID,用 `distFromStart` 变量记录从起点到当前节点的距离。 + +```java +class State { + // 图节点的 id + int id; + // 从 start 节点到当前节点的距离 + int distFromStart; + + State(int id, int distFromStart) { + this.id = id; + this.distFromStart = distFromStart; + } +} +``` + +## `distTo` 记录最短路径 + +加权图中的 Dijkstra 算法和无权图中的普通 BFS 算法不同,在 Dijkstra 算法中,你第一次经过某个节点时的路径权重,不见得就是最小的,所以对于同一个节点,我们可能会经过多次,而且每次的 `distFromStart` 可能都不一样,比如下图: + +![](https://labuladong.online/algo/images/dijkstra/3.jpeg) + +我会经过节点 `5` 三次,每次的 `distFromStart` 值都不一样,那我取 `distFromStart` 最小的那次,不就是从起点 `start` 到节点 `5` 的最短路径权重了么? + +所以我们需要一个 `distTo` 数组来记录从起点 `start` 到每个节点的最短路径权重和,起到一个备忘录的作用。 + +当重复遍历到同一个节点时,我们可以比较一下当前的 `distFromStart` 和 `distTo` 中的值,如果当前的更小,就更新 `distTo`,反之,就不用再往后继续遍历了。 + +## 代码实现 + +Dijkstra 的伪码逻辑如下: + +```java +// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离 +int[] dijkstra(int start, Graph graph) { + // 图中节点的个数 + int V = graph.size(); + // 记录最短路径的权重,你可以理解为 dp table + // 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重 + int[] distTo = new int[V]; + // 求最小值,所以 dp table 初始化为正无穷 + Arrays.fill(distTo, Integer.MAX_VALUE); + // base case,start 到 start 的最短距离就是 0 + distTo[start] = 0; + + // 优先级队列,distFromStart 较小的排在前面 + Queue pq = new PriorityQueue<>((a, b) -> { + return a.distFromStart - b.distFromStart; + }); + + // 从起点 start 开始进行 BFS + pq.offer(new State(start, 0)); + + while (!pq.isEmpty()) { + State curState = pq.poll(); + int curNodeID = curState.id; + int curDistFromStart = curState.distFromStart; + + if (curDistFromStart > distTo[curNodeID]) { + // 已经有一条更短的路径到达 curNode 节点了 + continue; + } + // 将 curNode 的相邻节点装入队列 + for (int nextNodeID : graph.neighbors(curNodeID)) { + // 看看从 curNode 达到 nextNode 的距离是否会更短 + int distToNextNode = distTo[curNodeID] + graph.weight(curNodeID, nextNodeID); + if (distTo[nextNodeID] > distToNextNode) { + // 更新 dp table + distTo[nextNodeID] = distToNextNode; + // 将这个节点以及距离放入队列 + pq.offer(new State(nextNodeID, distToNextNode)); + } + } + } + return distTo; +} +``` + +对比普通的 BFS 算法,你可能会有以下疑问: + +**1、没有 `visited` 集合记录已访问的节点,所以一个节点会被访问多次,会被多次加入队列,那会不会导致队列永远不为空,造成死循环**? + +**2、为什么用优先级队列 `PriorityQueue` 而不是 `LinkedList` 实现的普通队列?为什么要按照 `distFromStart` 的值来排序**? + +**3、如果我只想计算起点 `start` 到某一个终点 `end` 的最短路径,是否可以修改算法,提升一些效率**? + +我们先回答第一个问题,为什么这个算法不用 `visited` 集合也不会死循环。 + +对于这类问题,我教你一个思考方法: + +循环结束的条件是队列为空,那么你就要注意看什么时候往队列里放元素(调用 `offer` 方法),再注意看什么时候从队列往外拿元素(调用 `poll` 方法)。 + +`while` 循环每执行一次,都会往外拿一个元素,但想往队列里放元素,可就有很多限制了,必须满足下面这个条件: + +```java +// 看看从 curNode 达到 nextNode 的距离是否会更短 +if (distTo[nextNodeID] > distToNextNode) { + // 更新 dp table + distTo[nextNodeID] = distToNextNode; + pq.offer(new State(nextNodeID, distToNextNode)); +} +``` + +这也是为什么我说 `distTo` 数组可以理解成我们熟悉的 dp table,因为这个算法逻辑就是在不断的最小化 `distTo` 数组中的元素: + +如果你能让到达 `nextNodeID` 的距离更短,那就更新 `distTo[nextNodeID]` 的值,让你入队,否则的话对不起,不让入队。 + +**因为两个节点之间的最短距离(路径权重)肯定是一个确定的值,不可能无限减小下去,所以队列一定会空,队列空了之后,`distTo` 数组中记录的就是从 `start` 到其他节点的「最短距离」**。 + +接下来解答第二个问题,为什么要用 `PriorityQueue` 而不是 `LinkedList` 实现的普通队列? + +如果你非要用普通队列,其实也没问题的,你可以直接把 `PriorityQueue` 改成 `LinkedList`,也能得到正确答案,但是效率会低很多。 + +**Dijkstra 算法使用优先级队列,主要是为了效率上的优化,类似一种贪心算法的思路**。 + +为什么说是一种贪心思路呢,比如说下面这种情况,你想计算从起点 `start` 到终点 `end` 的最短路径权重: + + + + + +![](https://labuladong.online/algo/images/dijkstra/4.jpeg) + + + +假设你当前只遍历了图中的这几个节点,那么你下一步准备遍历那个节点?这三条路径都可能成为最短路径的一部分,**但你觉得哪条路径更有「潜力」成为最短路径中的一部分**? + +从目前的情况来看,显然橙色路径的可能性更大嘛,所以我们希望节点 `2` 排在队列靠前的位置,优先被拿出来向后遍历。 + +所以我们使用 `PriorityQueue` 作为队列,让 `distFromStart` 的值较小的节点排在前面,这就类似我们之前讲 [贪心算法](https://labuladong.online/algo/essential-technique/greedy/) 说到的贪心思路,可以很大程度上优化算法的效率。 + +大家应该听过 Bellman-Ford 算法,这个算法是一种更通用的最短路径算法,因为它可以处理带有负权重边的图,Bellman-Ford 算法逻辑和 Dijkstra 算法非常类似,用到的就是普通队列,本文就提一句,后面有空再具体写。 + +接下来说第三个问题,如果只关心起点 `start` 到某一个终点 `end` 的最短路径,是否可以修改代码提升算法效率。 + +肯定可以的,因为我们标准 Dijkstra 算法会算出 `start` 到所有其他节点的最短路径,你只想计算到 `end` 的最短路径,相当于减少计算量,当然可以提升效率。 + +需要在代码中做的修改也非常少,只要改改函数签名,再加个 if 判断就行了: + +```java +// 输入起点 start 和终点 end,计算起点到终点的最短距离 +int dijkstra(int start, int end, List[] graph) { + + // ... + + while (!pq.isEmpty()) { + State curState = pq.poll(); + int curNodeID = curState.id; + int curDistFromStart = curState.distFromStart; + + // 在这里加一个判断就行了,其他代码不用改 + if (curNodeID == end) { + return curDistFromStart; + } + + if (curDistFromStart > distTo[curNodeID]) { + continue; + } + + // ... + } + + // 如果运行到这里,说明从 start 无法走到 end + return Integer.MAX_VALUE; +} +``` + +因为优先级队列自动排序的性质,**每次**从队列里面拿出来的都是 `distFromStart` 值最小的,所以当你**第一次**从队列中拿出终点 `end` 时,此时的 `distFromStart` 对应的值就是从 `start` 到 `end` 的最短距离。 + +这个算法较之前的实现提前 return 了,所以效率有一定的提高。 + +这是 Dijkstra 算法的可视化面板,你可以点击其中的代码,查看算法的执行过程: + + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ +## 时间复杂度分析 + +Dijkstra 算法的时间复杂度是多少?你去网上查,可能会告诉你是 $O(ElogV)$,其中 `E` 代表图中边的条数,`V` 代表图中节点的个数。 + +因为理想情况下优先级队列中最多装 `V` 个节点,对优先级队列的操作次数和 `E` 成正比,所以整体的时间复杂度就是 $O(ElogV)$。 + +不过这是理想情况,Dijkstra 算法的代码实现有很多版本,不同编程语言或者不同数据结构 API 都会导致算法的时间复杂度发生一些改变。 + +比如本文实现的 Dijkstra 算法,使用了 Java 的 `PriorityQueue` 这个数据结构,这个容器类底层使用二叉堆实现,但没有提供通过索引操作队列中元素的 API,所以队列中会有重复的节点,最多可能有 `E` 个节点存在队列中。 + +所以本文实现的 Dijkstra 算法复杂度并不是理想情况下的 $O(ElogV)$,而是 $O(ElogE)$,可能会略大一些,因为图中边的条数一般是大于节点的个数的。 + +不过就对数函数来说,就算真数大一些,对数函数的结果也大不了多少,所以这个算法实现的实际运行效率也是很高的,以上只是理论层面的时间复杂度分析,供大家参考。 + +在下一节 [Dijkstra 算法习题](https://labuladong.online/algo/problem-set/dijkstra/) 中,我们会用 Dijkstra 算法解决一些具体的算法问题。 + + + + + + + +
+
+引用本文的文章 + + - [Kruskal 最小生成树算法](https://labuladong.online/algo/data-structure/kruskal/) + - [Prim 最小生成树算法](https://labuladong.online/algo/data-structure/prim/) + - [【强化练习】BFS 经典习题 II](https://labuladong.online/algo/problem-set/bfs-ii/) + - [【强化练习】Dijkstra 算法经典习题](https://labuladong.online/algo/problem-set/dijkstra/) + - [二分图判定算法](https://labuladong.online/algo/data-structure/bipartite-graph/) + - [二叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) + - [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + - [图结构基础及通用代码实现](https://labuladong.online/algo/data-structure-basic/graph-basic/) + - [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [旅游省钱大法:加权最短路径](https://labuladong.online/algo/dynamic-programming/cheap-travel/) + - [环检测及拓扑排序算法](https://labuladong.online/algo/data-structure/topological-sort/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1514. Path with Maximum Probability](https://leetcode.com/problems/path-with-maximum-probability/?show=1) | [1514. 概率最大的路径](https://leetcode.cn/problems/path-with-maximum-probability/?show=1) | 🟠 | +| [1631. Path With Minimum Effort](https://leetcode.com/problems/path-with-minimum-effort/?show=1) | [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/?show=1) | 🟠 | +| [286. Walls and Gates](https://leetcode.com/problems/walls-and-gates/?show=1)🔒 | [286. 墙与门](https://leetcode.cn/problems/walls-and-gates/?show=1)🔒 | 🟠 | +| [310. Minimum Height Trees](https://leetcode.com/problems/minimum-height-trees/?show=1) | [310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/?show=1) | 🟠 | +| [329. Longest Increasing Path in a Matrix](https://leetcode.com/problems/longest-increasing-path-in-a-matrix/?show=1) | [329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/?show=1) | 🔴 | +| [505. The Maze II](https://leetcode.com/problems/the-maze-ii/?show=1)🔒 | [505. 迷宫 II](https://leetcode.cn/problems/the-maze-ii/?show=1)🔒 | 🟠 | +| [542. 01 Matrix](https://leetcode.com/problems/01-matrix/?show=1) | [542. 01 矩阵](https://leetcode.cn/problems/01-matrix/?show=1) | 🟠 | +| [743. Network Delay Time](https://leetcode.com/problems/network-delay-time/?show=1) | [743. 网络延迟时间](https://leetcode.cn/problems/network-delay-time/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" index bb3262bc2d..00e2975e55 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" @@ -1,222 +1,133 @@ # 二叉堆详解实现优先级队列 -二叉堆(Binary Heap)没什么神秘,性质比二叉搜索树 BST 还简单。其主要操作就两个,`sink`(下沉)和 `swim`(上浮),用以维护二叉堆的性质。其主要应用有两个,首先是一种排序方法「堆排序」,第二是一种很有用的数据结构「优先级队列」。 +本文已重写,请阅读 [二叉堆基础原理](https://labuladong.online/algo/data-structure-basic/binary-heap-basic/) 和 [二叉堆实现优先级队列](https://labuladong.online/algo/data-structure-basic/binary-heap-implement/)。 -本文就以实现优先级队列(Priority Queue)为例,通过图片和人类的语言来描述一下二叉堆怎么运作的。 -### 一、二叉堆概览 -首先,二叉堆和二叉树有啥关系呢,为什么人们总数把二叉堆画成一棵二叉树? -因为,二叉堆其实就是一种特殊的二叉树(完全二叉树),只不过存储在数组里。一般的链表二叉树,我们操作节点的指针,而在数组里,我们把数组索引作为指针: -```java -// 父节点的索引 -int parent(int root) { - return root / 2; -} -// 左孩子的索引 -int left(int root) { - return root * 2; -} -// 右孩子的索引 -int right(int root) { - return root * 2 + 1; -} -``` +**_____________** -画个图你立即就能理解了,注意数组的第一个索引 0 空着不用, +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: -![1](../pictures/heap/1.png) +![](https://labuladong.online/algo/images/souyisou2.png) -PS:因为数组索引是数组,为了方便区分,将字符作为数组元素。 +======其他语言代码====== -你看到了,把 arr[1] 作为整棵树的根的话,每个节点的父节点和左右孩子的索引都可以通过简单的运算得到,这就是二叉堆设计的一个巧妙之处。为了方便讲解,下面都会画的图都是二叉树结构,相信你能把树和数组对应起来。 +### javascript -二叉堆还分为最大堆和最小堆。**最大堆的性质是:每个节点都大于等于它的两个子节点。**类似的,最小堆的性质是:每个节点都小于等于它的子节点。 - -两种堆核心思路都是一样的,本文以最大堆为例讲解。 - -对于一个最大堆,根据其性质,显然堆顶,也就是 arr[1] 一定是所有元素中最大的元素。 - -### 二、优先级队列概览 +```js +/** + * 最大堆 + */ +function left(i) { + return i * 2 + 1; +} -优先级队列这种数据结构有一个很有用的功能,你插入或者删除元素的时候,元素会自动排序,这底层的原理就是二叉堆的操作。 +function right(i) { + return i * 2 + 2; +} -数据结构的功能无非增删查该,优先级队列有两个主要 API,分别是 `insert` 插入一个元素和 `delMax` 删除最大元素(如果底层用最小堆,那么就是 `delMin`)。 +function swap(A, i, j) { + const t = A[i]; + A[i] = A[j]; + A[j] = t; +} -下面我们实现一个简化的优先级队列,先看下代码框架: +class Heap { + constructor(arr) { + this.data = [...arr]; + this.size = this.data.length; + } + + /** + * 重构堆 + */ + rebuildHeap() { + const L = Math.floor(this.size / 2); + for (let i = L - 1; i >= 0; i--) { + this.maxHeapify(i); + } + } -PS:为了清晰起见,这里用到 Java 的泛型,`Key` 可以是任何一种可比较大小的数据类型,你可以认为它是 int、char 等。 + isHeap() { + const L = Math.floor(this.size / 2); + for (let i = L - 1; i >= 0; i++) { + const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER; + const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER; -```java -public class MaxPQ - > { - // 存储元素的数组 - private Key[] pq; - // 当前 Priority Queue 中的元素个数 - private int N = 0; + const max = Math.max(this.data[i], l, r); - public MaxPQ(int cap) { - // 索引 0 不用,所以多分配一个空间 - pq = (Key[]) new Comparable[cap + 1]; + if (max !== this.data[i]) { + return false; + } + return true; } + } - /* 返回当前队列中最大元素 */ - public Key max() { - return pq[1]; + sort() { + for (let i = this.size - 1; i > 0; i--) { + swap(this.data, 0, i); + this.size--; + this.maxHeapify(0); } + } - /* 插入元素 e */ - public void insert(Key e) {...} - - /* 删除并返回当前队列中最大元素 */ - public Key delMax() {...} - - /* 上浮第 k 个元素,以维护最大堆性质 */ - private void swim(int k) {...} - - /* 下沉第 k 个元素,以维护最大堆性质 */ - private void sink(int k) {...} - - /* 交换数组的两个元素 */ - private void exch(int i, int j) { - Key temp = pq[i]; - pq[i] = pq[j]; - pq[j] = temp; + insert(key) { + this.data[this.size++] = key; + if (this.isHeap()) { + return; } + this.rebuildHeap(); + } - /* pq[i] 是否比 pq[j] 小? */ - private boolean less(int i, int j) { - return pq[i].compareTo(pq[j]) < 0; + delete(index) { + if (index >= this.size) { + return; } - - /* 还有 left, right, parent 三个方法 */ -} -``` - -空出来的四个方法是二叉堆和优先级队列的奥妙所在,下面用图文来逐个理解。 - -### 三、实现 swim 和 sink - -为什么要有上浮 swim 和下沉 sink 的操作呢?为了维护堆结构。 - -我们要讲的是最大堆,每个节点都比它的两个子节点大,但是在插入元素和删除元素时,难免破坏堆的性质,这就需要通过这两个操作来恢复堆的性质了。 - -对于最大堆,会破坏堆性质的有有两种情况: - -1. 如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行**下沉**。 - -2. 如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的**上浮**。 - -当然,错位的节点 A 可能要上浮(或下沉)很多次,才能到达正确的位置,恢复堆的性质。所以代码中肯定有一个 `while` 循环。 - -细心的读者也许会问,这两个操作不是互逆吗,所以上浮的操作一定能用下沉来完成,为什么我还要费劲写两个方法? - -是的,操作是互逆等价的,但是最终我们的操作只会在堆底和堆顶进行(等会讲原因),显然堆底的「错位」元素需要上浮,堆顶的「错位」元素需要下沉。 - -**上浮的代码实现:** - -```java -private void swim(int k) { - // 如果浮到堆顶,就不能再上浮了 - while (k > 1 && less(parent(k), k)) { - // 如果第 k 个元素比上层大 - // 将 k 换上去 - exch(parent(k), k); - k = parent(k); + this.data.splice(index, 1); + this.size--; + if (this.isHeap()) { + return; } -} -``` - -画个 GIF 看一眼就明白了: - -![2](../pictures/heap/swim.gif) - -**下沉的代码实现:** - -下沉比上浮略微复杂一点,因为上浮某个节点 A,只需要 A 和其父节点比较大小即可;但是下沉某个节点 A,需要 A 和其**两个子节点**比较大小,如果 A 不是最大的就需要调整位置,要把较大的那个子节点和 A 交换。 - -```java -private void sink(int k) { - // 如果沉到堆底,就沉不下去了 - while (left(k) <= N) { - // 先假设左边节点较大 - int older = left(k); - // 如果右边节点存在,比一下大小 - if (right(k) <= N && less(older, right(k))) - older = right(k); - // 结点 k 比俩孩子都大,就不必下沉了 - if (less(older, k)) break; - // 否则,不符合最大堆的结构,下沉 k 结点 - exch(k, older); - k = older; + this.rebuildHeap(); + } + + /** + * 堆的其他地方都满足性质 + * 唯独跟节点,重构堆性质 + * @param {*} i + */ + maxHeapify(i) { + let max = i; + + if (i >= this.size) { + return; } -} -``` - -画个 GIF 看下就明白了: - -![3](../pictures/heap/sink.gif) - -至此,二叉堆的主要操作就讲完了,一点都不难吧,代码加起来也就十行。明白了 `sink` 和 `swim` 的行为,下面就可以实现优先级队列了。 + // 求左右节点中较大的序号 + const l = left(i); + const r = right(i); + if (l < this.size && this.data[l] > this.data[max]) { + max = l; + } -### 四、实现 delMax 和 insert - -这两个方法就是建立在 `swim` 和 `sink` 上的。 + if (r < this.size && this.data[r] > this.data[max]) { + max = r; + } -**`insert` 方法先把要插入的元素添加到堆底的最后,然后让其上浮到正确位置。** + // 如果当前节点最大,已经是最大堆 + if (max === i) { + return; + } -![4](../pictures/heap/insert.gif) + swap(this.data, i, max); -```java -public void insert(Key e) { - N++; - // 先把新元素加到最后 - pq[N] = e; - // 然后让它上浮到正确的位置 - swim(N); + // 递归向下继续执行 + return this.maxHeapify(max); + } } -``` -**`delMax` 方法先把堆顶元素 A 和堆底最后的元素 B 对调,然后删除 A,最后让 B 下沉到正确位置。** - -```java -public Key delMax() { - // 最大堆的堆顶就是最大元素 - Key max = pq[1]; - // 把这个最大元素换到最后,删除之 - exch(1, N); - pq[N] = null; - N--; - // 让 pq[1] 下沉到正确位置 - sink(1); - return max; -} +module.exports = Heap; ``` -![5](../pictures/heap/delete.gif) - -至此,一个优先级队列就实现了,插入和删除元素的时间复杂度为 $O(logK)$,$K$ 为当前二叉堆(优先级队列)中的元素总数。因为我们时间复杂度主要花费在 `sink` 或者 `swim` 上,而不管上浮还是下沉,最多也就树(堆)的高度,也就是 log 级别。 - - -### 五、最后总结 - -二叉堆就是一种完全二叉树,所以适合存储在数组中,而且二叉堆拥有一些特殊性质。 - -二叉堆的操作很简单,主要就是上浮和下沉,来维护堆的性质(堆有序),核心代码也就十行。 - -优先级队列是基于二叉堆实现的,主要操作是插入和删除。插入是先插到最后,然后上浮到正确位置;删除是调换位置后再删除,然后下沉到正确位置。核心代码也就十行。 - -也许这就是数据结构的威力,简单的操作就能实现巧妙的功能,真心佩服发明二叉堆算法的人! - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) - -[上一篇:学习数据结构和算法读什么书](../算法思维系列/为什么推荐算法4.md) - -[下一篇:LRU算法详解](../高频面试系列/LRU算法.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\346\223\215\344\275\234\351\233\206\351\224\246.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\346\223\215\344\275\234\351\233\206\351\224\246.md" deleted file mode 100644 index 3fd21e6d74..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\346\223\215\344\275\234\351\233\206\351\224\246.md" +++ /dev/null @@ -1,284 +0,0 @@ -# 二叉搜索树操作集锦 - -通过之前的文章[框架思维](../算法思维系列/学习数据结构和算法的高效方法.md),二叉树的遍历框架应该已经印到你的脑子里了,这篇文章就来实操一下,看看框架思维是怎么灵活运用,秒杀一切二叉树问题的。 - -二叉树算法的设计的总路线:明确一个节点要做的事情,然后剩下的事抛给框架。 - -```java -void traverse(TreeNode root) { - // root 需要做什么?在这做。 - // 其他的不用 root 操心,抛给框架 - traverse(root.left); - traverse(root.right); -} -``` - -举两个简单的例子体会一下这个思路,热热身。 - -**1. 如何把二叉树所有的节点中的值加一?** - -```java -void plusOne(TreeNode root) { - if (root == null) return; - root.val += 1; - - plusOne(root.left); - plusOne(root.right); -} -``` - -**2. 如何判断两棵二叉树是否完全相同?** - -```java -boolean isSameTree(TreeNode root1, TreeNode root2) { - // 都为空的话,显然相同 - if (root1 == null && root2 == null) return true; - // 一个为空,一个非空,显然不同 - if (root1 == null || root2 == null) return false; - // 两个都非空,但 val 不一样也不行 - if (root1.val != root2.val) return false; - - // root1 和 root2 该比的都比完了 - return isSameTree(root1.left, root2.left) - && isSameTree(root1.right, root2.right); -} -``` - -借助框架,上面这两个例子不难理解吧?如果可以理解,那么所有二叉树算法你都能解决。 - - - -二叉搜索树(Binary Search Tree,简称 BST)是一种很常用的的二叉树。它的定义是:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,且要小于等于右边子树的所有节点的值。 - -如下就是一个符合定义的 BST: - -![BST](../pictures/BST/BST_example.png) - - -下面实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中“删”和“判断合法性”略微复杂。 - -**零、判断 BST 的合法性** - -这里是有坑的哦,我们按照刚才的思路,每个节点自己要做的事不就是比较自己和左右孩子吗?看起来应该这样写代码: -```java -boolean isValidBST(TreeNode root) { - if (root == null) return true; - if (root.left != null && root.val <= root.left.val) return false; - if (root.right != null && root.val >= root.right.val) return false; - - return isValidBST(root.left) - && isValidBST(root.right); -} -``` - -但是这个算法出现了错误,BST 的每个节点应该要小于右边子树的所有节点,下面这个二叉树显然不是 BST,但是我们的算法会把它判定为 BST。 - -![notBST](../pictures/BST/假BST.png) - -出现错误,不要慌张,框架没有错,一定是某个细节问题没注意到。我们重新看一下 BST 的定义,root 需要做的不只是和左右子节点比较,而是要整个左子树和右子树所有节点比较。怎么办,鞭长莫及啊! - -这种情况,我们可以使用辅助函数,增加函数参数列表,在参数中携带额外信息,请看正确的代码: - -```java -boolean isValidBST(TreeNode root) { - return isValidBST(root, null, null); -} - -boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) { - if (root == null) return true; - if (min != null && root.val <= min.val) return false; - if (max != null && root.val >= max.val) return false; - return isValidBST(root.left, min, root) - && isValidBST(root.right, root, max); -} -``` - - -**一、在 BST 中查找一个数是否存在** - -根据我们的指导思想,可以这样写代码: - -```java -boolean isInBST(TreeNode root, int target) { - if (root == null) return false; - if (root.val == target) return true; - - return isInBST(root.left, target) - || isInBST(root.right, target); -} -``` - -这样写完全正确,充分证明了你的框架性思维已经养成。现在你可以考虑一点细节问题了:如何充分利用信息,把 BST 这个“左小右大”的特性用上? - -很简单,其实不需要递归地搜索两边,类似二分查找思想,根据 target 和 root.val 的大小比较,就能排除一边。我们把上面的思路稍稍改动: - -```java -boolean isInBST(TreeNode root, int target) { - if (root == null) return false; - if (root.val == target) - return true; - if (root.val < target) - return isInBST(root.right, target); - if (root.val > target) - return isInBST(root.left, target); - // root 该做的事做完了,顺带把框架也完成了,妙 -} -``` - -于是,我们对原始框架进行改造,抽象出一套**针对 BST 的遍历框架**: - -```java -void BST(TreeNode root, int target) { - if (root.val == target) - // 找到目标,做点什么 - if (root.val < target) - BST(root.right, target); - if (root.val > target) - BST(root.left, target); -} -``` - - -**二、在 BST 中插入一个数** - -对数据结构的操作无非遍历 + 访问,遍历就是“找”,访问就是“改”。具体到这个问题,插入一个数,就是先找到插入位置,然后进行插入操作。 - -上一个问题,我们总结了 BST 中的遍历框架,就是“找”的问题。直接套框架,加上“改”的操作即可。一旦涉及“改”,函数就要返回 TreeNode 类型,并且对递归调用的返回值进行接收。 - -```java -TreeNode insertIntoBST(TreeNode root, int val) { - // 找到空位置插入新节点 - if (root == null) return new TreeNode(val); - // if (root.val == val) - // BST 中一般不会插入已存在元素 - if (root.val < val) - root.right = insertIntoBST(root.right, val); - if (root.val > val) - root.left = insertIntoBST(root.left, val); - return root; -} -``` - - -**三、在 BST 中删除一个数** - -这个问题稍微复杂,不过你有框架指导,难不住你。跟插入操作类似,先“找”再“改”,先把框架写出来再说: - -```java -TreeNode deleteNode(TreeNode root, int key) { - if (root.val == key) { - // 找到啦,进行删除 - } else if (root.val > key) { - root.left = deleteNode(root.left, key); - } else if (root.val < key) { - root.right = deleteNode(root.right, key); - } - return root; -} -``` - -找到目标节点了,比方说是节点 A,如何删除这个节点,这是难点。因为删除节点的同时不能破坏 BST 的性质。有三种情况,用图片来说明。 - -情况 1:A 恰好是末端节点,两个子节点都为空,那么它可以当场去世了。 - -图片来自 LeetCode -![1](../pictures/BST/bst_deletion_case_1.png) - -```java -if (root.left == null && root.right == null) - return null; -``` - -情况 2:A 只有一个非空子节点,那么它要让这个孩子接替自己的位置。 - -图片来自 LeetCode -![2](../pictures/BST/bst_deletion_case_2.png) - -```java -// 排除了情况 1 之后 -if (root.left == null) return root.right; -if (root.right == null) return root.left; -``` - -情况 3:A 有两个子节点,麻烦了,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。 - -图片来自 LeetCode -![2](../pictures/BST/bst_deletion_case_3.png) - -```java -if (root.left != null && root.right != null) { - // 找到右子树的最小节点 - TreeNode minNode = getMin(root.right); - // 把 root 改成 minNode - root.val = minNode.val; - // 转而去删除 minNode - root.right = deleteNode(root.right, minNode.val); -} -``` - -三种情况分析完毕,填入框架,简化一下代码: - -```java -TreeNode deleteNode(TreeNode root, int key) { - if (root == null) return null; - if (root.val == key) { - // 这两个 if 把情况 1 和 2 都正确处理了 - if (root.left == null) return root.right; - if (root.right == null) return root.left; - // 处理情况 3 - TreeNode minNode = getMin(root.right); - root.val = minNode.val; - root.right = deleteNode(root.right, minNode.val); - } else if (root.val > key) { - root.left = deleteNode(root.left, key); - } else if (root.val < key) { - root.right = deleteNode(root.right, key); - } - return root; -} - -TreeNode getMin(TreeNode node) { - // BST 最左边的就是最小的 - while (node.left != null) node = node.left; - return node; -} -``` - -删除操作就完成了。注意一下,这个删除操作并不完美,因为我们一般不会通过 root.val = minNode.val 修改节点内部的值来交换节点,而是通过一系列略微复杂的链表操作交换 root 和 minNode 两个节点。因为具体应用中,val 域可能会很大,修改起来很耗时,而链表操作无非改一改指针,而不会去碰内部数据。 - -但这里忽略这个细节,旨在突出 BST 基本操作的共性,以及借助框架逐层细化问题的思维方式。 - -**四、最后总结** - -通过这篇文章,你学会了如下几个技巧: - -1. 二叉树算法设计的总路线:把当前节点要做的事做好,其他的交给递归框架,不用当前节点操心。 - -2. 如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。 - -3. 在二叉树框架之上,扩展出一套 BST 遍历框架: -```java -void BST(TreeNode root, int target) { - if (root.val == target) - // 找到目标,做点什么 - if (root.val < target) - BST(root.right, target); - if (root.val > target) - BST(root.left, target); -} -``` - -4. 掌握了 BST 的基本操作。 - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:LRU算法详解](../高频面试系列/LRU算法.md) - -[下一篇:特殊数据结构:单调栈](../数据结构系列/单调栈.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..a08aea4705 --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" @@ -0,0 +1,1164 @@ +# 二叉树系列算法核心纲领 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [104. Maximum Depth of Binary Tree](https://leetcode.com/problems/maximum-depth-of-binary-tree/) | [104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | 🟢 | +| [144. Binary Tree Preorder Traversal](https://leetcode.com/problems/binary-tree-preorder-traversal/) | [144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | 🟢 | +| [543. Diameter of Binary Tree](https://leetcode.com/problems/diameter-of-binary-tree/) | [543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树结构基础](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) +> - [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) + +> [!IMPORTANT] +> 本文会把很多算法进行抽象和归纳,所以会包含大量其他文章链接。 +> +> **第一次阅读本文的读者不要 DFS 学习本文,遇到没学过的算法或不理解的地方请跳过,只要对本文所总结的理论有些印象即可**。在学习本站后面的算法技巧时,你自然可以逐渐理解本文的精髓所在,日后回来重读本文,会有更深的体会。 + +本站所有文章的脉络都是按照 [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) 提出的框架来构建的,其中着重强调了二叉树题目的重要性,所以把本文放在第一章的必读系列中。 + +我刷了这么多年题,浓缩出二叉树算法的一个总纲放在这里,也许用词不是特别专业化,也没有什么教材会收录我的这些经验总结,但目前各个刷题平台的题库,没有一道二叉树题目能跳出本文划定的框架。如果你能发现一道题目和本文给出的框架不兼容,请留言告知我。 + +先在开头总结一下,二叉树解题的思维模式分两类: + +**1、是否可以通过遍历一遍二叉树得到答案**?如果可以,用一个 `traverse` 函数配合外部变量来实现,这叫「遍历」的思维模式。 + +**2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案**?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。 + +无论使用哪种思维模式,你都需要思考: + +**如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做**?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。 + +本文中会用题目来举例,但都是最最简单的题目,所以不用担心自己看不懂,我可以帮你从最简单的问题中提炼出所有二叉树题目的共性,并将二叉树中蕴含的思维进行升华,反手用到 [动态规划](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/),[回溯算法](https://labuladong.online/algo/essential-technique/backtrack-framework/),[分治算法](https://labuladong.online/algo/essential-technique/divide-and-conquer/),[图论算法](https://labuladong.online/algo/data-structure-basic/graph-basic/) 中去,这也是我一直强调框架思维的原因。希望你在学习了上述高级算法后,也能回头再来看看本文,会对它们有更深刻的认识。 + +首先,我还是要不厌其烦地强调一下二叉树这种数据结构及相关算法的重要性。 + + + + + + + +## 二叉树的重要性 + +举个例子,比如两个经典排序算法 [快速排序](https://labuladong.online/algo/practice-in-action/quick-sort/) 和 [归并排序](https://labuladong.online/algo/practice-in-action/merge-sort/),对于它俩,你有什么理解? + +**如果你告诉我,快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历,那么我就知道你是个算法高手了**。 + +为什么快速排序和归并排序能和二叉树扯上关系?我们来简单分析一下他们的算法思想和代码框架: + +快速排序的逻辑是,若要对 `nums[lo..hi]` 进行排序,我们先找一个分界点 `p`,通过交换元素使得 `nums[lo..p-1]` 都小于等于 `nums[p]`,且 `nums[p+1..hi]` 都大于 `nums[p]`,然后递归地去 `nums[lo..p-1]` 和 `nums[p+1..hi]` 中寻找新的分界点,最后整个数组就被排序了。 + +快速排序的代码框架如下: + +```java +void sort(int[] nums, int lo, int hi) { + // ****** 前序遍历位置 ****** + // 通过交换元素构建分界点 p + int p = partition(nums, lo, hi); + // ************************ + + sort(nums, lo, p - 1); + sort(nums, p + 1, hi); +} +``` + +先构造分界点,然后去左右子数组构造分界点,你看这不就是一个二叉树的前序遍历吗? + +再说说归并排序的逻辑,若要对 `nums[lo..hi]` 进行排序,我们先对 `nums[lo..mid]` 排序,再对 `nums[mid+1..hi]` 排序,最后把这两个有序的子数组合并,整个数组就排好序了。 + +归并排序的代码框架如下: + +```java +// 定义:排序 nums[lo..hi] +void sort(int[] nums, int lo, int hi) { + int mid = (lo + hi) / 2; + // 排序 nums[lo..mid] + sort(nums, lo, mid); + // 排序 nums[mid+1..hi] + sort(nums, mid + 1, hi); + + // ****** 后序位置 ****** + // 合并 nums[lo..mid] 和 nums[mid+1..hi] + merge(nums, lo, mid, hi); + // ********************* +} +``` + +先对左右子数组排序,然后合并(类似合并有序链表的逻辑),你看这是不是二叉树的后序遍历框架?另外,这不就是传说中的分治算法嘛,不过如此呀。 + +如果你一眼就识破这些排序算法的底细,还需要背这些经典算法吗?不需要。你可以手到擒来,从二叉树遍历框架就能扩展出算法了。 + +说了这么多,旨在说明,二叉树的算法思想的运用广泛,甚至可以说,只要涉及递归,都可以抽象成二叉树的问题。 + +接下来我们从二叉树的前中后序开始讲起,让你深刻理解这种数据结构的魅力。 + +## 深入理解前中后序 + +我先甩给你几个问题,请默默思考 30 秒: + +1、你理解的二叉树的前中后序遍历是什么,仅仅是三个顺序不同的 List 吗? + +2、请分析,后序遍历有什么特殊之处? + +3、请分析,为什么多叉树没有中序遍历? + +答不上来,说明你对前中后序的理解仅仅局限于教科书,不过没关系,我用类比的方式解释一下我眼中的前中后序遍历。 + +首先,回顾一下 [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) 中说到的二叉树递归遍历框架: + +```java +void traverse(TreeNode root) { + if (root == null) { + return; + } + // 前序位置 + traverse(root.left); + // 中序位置 + traverse(root.right); + // 后序位置 +} +``` + +先不管所谓前中后序,单看 `traverse` 函数,你说它在做什么事情? + +其实它就是一个能够遍历二叉树所有节点的一个函数,和你遍历数组或者链表本质上没有区别: + +```java +// 迭代遍历数组 +void traverse(int[] arr) { + for (int i = 0; i < arr.length; i++) { + + } +} + +// 递归遍历数组 +void traverse(int[] arr, int i) { + if (i == arr.length) { + return; + } + // 前序位置 + traverse(arr, i + 1); + // 后序位置 +} + +// 迭代遍历单链表 +void traverse(ListNode head) { + for (ListNode p = head; p != null; p = p.next) { + + } +} + +// 递归遍历单链表 +void traverse(ListNode head) { + if (head == null) { + return; + } + // 前序位置 + traverse(head.next); + // 后序位置 +} +``` + +单链表和数组的遍历可以是迭代的,也可以是递归的,**二叉树这种结构无非就是二叉链表**,它没办法简单改写成 for 循环的迭代形式,所以我们遍历二叉树一般都使用递归形式。 + +你也注意到了,只要是递归形式的遍历,都可以有前序位置和后序位置,分别在递归之前和递归之后。 + +**所谓前序位置,就是刚进入一个节点(元素)的时候,后序位置就是即将离开一个节点(元素)的时候**,那么进一步,你把代码写在不同位置,代码执行的时机也不同: + +![](https://labuladong.online/algo/images/binary-tree-summary/1.jpeg) + +比如说,如果让你**倒序打印**一条单链表上所有节点的值,你怎么搞? + +实现方式当然有很多,但如果你对递归的理解足够透彻,可以利用后序位置来操作: + +```java +// 递归遍历单链表,倒序打印链表元素 +void traverse(ListNode head) { + if (head == null) { + return; + } + traverse(head.next); + // 后序位置 + print(head.val); +} +``` + +结合上面那张图,你应该知道为什么这段代码能够倒序打印单链表了吧,本质上是利用递归的堆栈帮你实现了倒序遍历的效果。 + +那么说回二叉树也是一样的,只不过多了一个中序位置罢了。 + +教科书里只会问你前中后序遍历结果分别是什么,所以对于一个只上过大学数据结构课程的人来说,他大概以为二叉树的前中后序只不过对应三种顺序不同的 `List` 列表。 + +但是我想说,**前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点**,绝不仅仅是三个顺序不同的 List: + +前序位置的代码在刚刚进入一个二叉树节点的时候执行; + +后序位置的代码在将要离开一个二叉树节点的时候执行; + +中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。 + +你注意本文的用词,我一直说前中后序「位置」,就是要和大家常说的前中后序「遍历」有所区别:你可以在前序位置写代码往一个 List 里面塞元素,那最后得到的就是前序遍历结果;但并不是说你就不可以写更复杂的代码做更复杂的事。 + +画成图,前中后序三个位置在二叉树上是这样: + +![](https://labuladong.online/algo/images/binary-tree-summary/2.jpeg) + +**你可以发现每个节点都有「唯一」属于自己的前中后序位置**,所以我说前中后序遍历是遍历二叉树过程中处理每一个节点的三个特殊时间点。 + +这里你也可以理解为什么多叉树没有中序位置,因为二叉树的每个节点只会进行唯一一次左子树切换右子树,而多叉树节点可能有很多子节点,会多次切换子树去遍历,所以多叉树节点没有「唯一」的中序遍历位置。 + +说了这么多基础的,就是要帮你对二叉树建立正确的认识,然后你会发现: + +**二叉树的所有问题,就是让你在前中后序位置注入巧妙的代码逻辑,去达到自己的目的,你只需要单独思考每一个节点应该做什么,其他的不用你管,抛给二叉树遍历框架,递归会在所有节点上做相同的操作**。 + +你也可以看到,[图论算法基础](https://labuladong.online/algo/data-structure-basic/graph-basic/) 把二叉树的遍历框架扩展到了图,并以遍历为基础实现了图论的各种经典算法,不过这是后话,本文就不多说了。 + + + + + + + +## 两种解题思路 + +前文 [我的算法学习心得](https://labuladong.online/algo/essential-technique/algorithm-summary/) 说过: + +**二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 [回溯算法核心框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) 和 [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/)**。 + +> [!TIP] +> 这里说一下我的函数命名习惯:二叉树中用遍历思路解题时函数签名一般是 `void traverse(...)`,没有返回值,靠更新外部变量来计算结果,而用分解问题思路解题时函数名根据该函数具体功能而定,而且一般会有返回值,返回值是子问题的计算结果。 +> +> 与此对应的,你会发现我在 [回溯算法核心框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) 中给出的函数签名一般也是没有返回值的 `void backtrack(...)`,而在 [动态规划核心框架](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 中给出的函数签名是带有返回值的 `dp` 函数。这也说明它俩和二叉树之间千丝万缕的联系。 +> +> 虽然函数命名没有什么硬性的要求,但我还是建议你也遵循我的这种风格,这样更能突出函数的作用和解题的思维模式,便于你自己理解和运用。 + +当时我是用二叉树的最大深度这个问题来举例,重点在于把这两种思路和动态规划和回溯算法进行对比,而本文的重点在于分析这两种思路如何解决二叉树的题目。 + +力扣第 104 题「二叉树的最大深度」就是最大深度的题目,所谓最大深度就是根节点到「最远」叶子节点的最长路径上的节点数,比如输入这棵二叉树,算法应该返回 3: + +![](https://labuladong.online/algo/images/binary-tree-summary/tree.jpg) + +你做这题的思路是什么?显然遍历一遍二叉树,用一个外部变量记录每个节点所在的深度,取最大值就可以得到最大深度,**这就是遍历二叉树计算答案的思路**。 + +解法代码如下: + +```java +class Solution { + // 记录最大深度 + int res = 0; + + // 记录遍历到的节点的深度 + int depth = 0; + + public int maxDepth(TreeNode root) { + traverse(root); + return res; + } + + // 二叉树遍历框架 + void traverse(TreeNode root) { + if (root == null) { + return; + } + // 前序位置 + depth++; + if (root.left == null && root.right == null) { + // 到达叶子节点,更新最大深度 + res = Math.max(res, depth); + } + traverse(root.left); + traverse(root.right); + // 后序位置 + depth--; + } +} +``` + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ +这个解法应该很好理解,但为什么需要在前序位置增加 `depth`,在后序位置减小 `depth`? + +因为前面说了,前序位置是进入一个节点的时候,后序位置是离开一个节点的时候,`depth` 记录当前递归到的节点深度,你把 `traverse` 理解成在二叉树上游走的一个指针,所以当然要这样维护。 + +至于对 `res` 的更新,你放到前中后序位置都可以,只要保证在进入节点之后,离开节点之前(即 `depth` 自增之后,自减之前)就行了。 + +当然,你也很容易发现一棵二叉树的最大深度可以通过子树的最大深度推导出来,**这就是分解问题计算答案的思路**。 + +解法代码如下: + +```java +class Solution { + // 定义:输入根节点,返回这棵二叉树的最大深度 + public int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + // 利用定义,计算左右子树的最大深度 + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + // 整棵树的最大深度等于左右子树的最大深度取最大值, + // 然后再加上根节点自己 + int res = Math.max(leftMax, rightMax) + 1; + + return res; + } +} +``` + + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ +只要明确递归函数的定义,这个解法也不难理解,但为什么主要的代码逻辑集中在后序位置? + +因为这个思路正确的核心在于,你确实可以通过子树的最大深度推导出原树的深度,所以当然要首先利用递归函数的定义算出左右子树的最大深度,然后推出原树的最大深度,主要逻辑自然放在后序位置。 + +如果你理解了最大深度这个问题的两种思路,**那么我们再回头看看最基本的二叉树前中后序遍历**,就比如力扣第 144 题「二叉树的前序遍历」,让你计算前序遍历结果。 + +我们熟悉的解法就是用「遍历」的思路,我想应该没什么好说的: + +```java +class Solution { + // 存放前序遍历结果 + List res = new LinkedList<>(); + + // 返回前序遍历结果 + public List preorderTraversal(TreeNode root) { + traverse(root); + return res; + } + + // 二叉树遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + // 前序位置 + res.add(root.val); + traverse(root.left); + traverse(root.right); + } +} +``` + +但你是否能够用「分解问题」的思路,来计算前序遍历的结果? + +换句话说,不要用像 `traverse` 这样的辅助函数和任何外部变量,单纯用题目给的 `preorderTraverse` 函数递归解题,你会不会? + +我们知道前序遍历的特点是,根节点的值排在首位,接着是左子树的前序遍历结果,最后是右子树的前序遍历结果: + +![](https://labuladong.online/algo/images/binary-tree-summary/3.jpeg) + +那这不就可以分解问题了么,**一棵二叉树的前序遍历结果 = 根节点 + 左子树的前序遍历结果 + 右子树的前序遍历结果**。 + +所以,你可以这样实现前序遍历算法: + +```java +class Solution { + // 定义:输入一棵二叉树的根节点,返回这棵树的前序遍历结果 + List preorderTraversal(TreeNode root) { + List res = new LinkedList<>(); + if (root == null) { + return res; + } + // 前序遍历的结果,root.val 在第一个 + res.add(root.val); + // 利用函数定义,后面接着左子树的前序遍历结果 + res.addAll(preorderTraversal(root.left)); + // 利用函数定义,最后接着右子树的前序遍历结果 + res.addAll(preorderTraversal(root.right)); + return res; + } +} +``` + +中序和后序遍历也是类似的,只要把 `add(root.val)` 放到中序和后序对应的位置就行了。 + +这个解法短小精干,但为什么不常见呢? + +一个原因是**这个算法的复杂度不好把控**,比较依赖语言特性。 + +Java 的话无论 ArrayList 还是 LinkedList,`addAll` 方法的复杂度都是 O(N),所以总体的最坏时间复杂度会达到 O(N^2),除非你自己实现一个复杂度为 O(1) 的 `addAll` 方法,底层用链表的话是可以做到的,因为多条链表只要简单的指针操作就能连接起来。 + +当然,最主要的原因还是因为教科书上从来没有这么教过…… + +上文举了两个简单的例子,但还有不少二叉树的题目是可以同时使用两种思路来思考和求解的,这就要靠你自己多去练习和思考,不要仅仅满足于一种熟悉的解法思路。 + +综上,遇到一道二叉树的题目时的通用思考过程是: + +**1、是否可以通过遍历一遍二叉树得到答案**?如果可以,用一个 `traverse` 函数配合外部变量来实现。 + +**2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案**?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。 + +**3、无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做**。 + +本站 [二叉树递归专项练习](https://labuladong.online/algo/intro/binary-tree-practice/) 中列举了 100 多道二叉树习题,完全使用上述两种思维模式手把手带你练习,助你完全掌握递归思维,更容易理解高级的算法。 + + + + + + + +## 后序位置的特殊之处 + +说后序位置之前,先简单说下前序和中序。 + +前序位置本身其实没有什么特别的性质,之所以你发现好像很多题都是在前序位置写代码,实际上是因为我们习惯把那些对前中后序位置不敏感的代码写在前序位置罢了。 + +中序位置主要用在 BST 场景中,你完全可以把 BST 的中序遍历认为是遍历有序数组。 + +> [!IMPORTANT] +> **仔细观察,前中后序位置的代码,能力依次增强**。 +> +> 前序位置的代码只能从函数参数中获取父节点传递来的数据。 +> +> 中序位置的代码不仅可以获取参数数据,还可以获取到左子树通过函数返回值传递回来的数据。 +> +> 后序位置的代码最强,不仅可以获取参数数据,还可以同时获取到左右子树通过函数返回值传递回来的数据。 +> +> 所以,某些情况下把代码移到后序位置效率最高;有些事情,只有后序位置的代码能做。 + +举些具体的例子来感受下它们的能力区别。现在给你一棵二叉树,我问你两个简单的问题: + +1、如果把根节点看做第 1 层,如何打印出每一个节点所在的层数? + +2、如何打印出每个节点的左右子树各有多少节点? + +第一个问题可以这样写代码: + +```java +// 二叉树遍历函数 +void traverse(TreeNode root, int level) { + if (root == null) { + return; + } + // 前序位置 + printf("Node %s at level %d", root.val, level); + traverse(root.left, level + 1); + traverse(root.right, level + 1); +} + +// 这样调用 +traverse(root, 1); +``` + +第二个问题可以这样写代码: + +```java +// 定义:输入一棵二叉树,返回这棵二叉树的节点总数 +int count(TreeNode root) { + if (root == null) { + return 0; + } + int leftCount = count(root.left); + int rightCount = count(root.right); + // 后序位置 + printf("节点 %s 的左子树有 %d 个节点,右子树有 %d 个节点", + root, leftCount, rightCount); + + return leftCount + rightCount + 1; +} +``` + +> [!NOTE] +> 一个节点在第几层,你从根节点遍历过来的过程就能顺带记录,用递归函数的参数就能传递下去;而以一个节点为根的整棵子树有多少个节点,你必须遍历完子树之后才能数清楚,然后通过递归函数的返回值拿到答案。 +> +> 结合这两个简单的问题,你品味一下后序位置的特点,只有后序位置才能通过返回值获取子树的信息。 +> +> **那么换句话说,一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了**。 + +接下来看下后序位置是如何在实际的题目中发挥作用的,简单聊下力扣第 543 题「二叉树的直径」,让你计算一棵二叉树的最长直径长度。 + +所谓二叉树的「直径」长度,就是任意两个结点之间的路径长度。最长「直径」并不一定要穿过根结点,比如下面这棵二叉树: + +![](https://labuladong.online/algo/images/binary-tree-summary/tree1.png) + +它的最长直径是 3,即 `[4,2,1,3]`,`[4,2,1,9]` 或者 `[5,2,1,3]` 这几条「直径」的长度。 + +解决这题的关键在于,**每一条二叉树的「直径」长度,就是一个节点的左右子树的最大深度之和**。 + +现在让我求整棵树中的最长「直径」,那直截了当的思路就是遍历整棵树中的每个节点,然后通过每个节点的左右子树的最大深度算出每个节点的「直径」,最后把所有「直径」求个最大值即可。 + +最大深度的算法我们刚才实现过了,上述思路就可以写出以下代码: + +```java +class Solution { + // 记录最大直径的长度 + int maxDiameter = 0; + + public int diameterOfBinaryTree(TreeNode root) { + // 对每个节点计算直径,求最大直径 + traverse(root); + return maxDiameter; + } + + // 遍历二叉树 + void traverse(TreeNode root) { + if (root == null) { + return; + } + // 对每个节点计算直径 + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + int myDiameter = leftMax + rightMax; + // 更新全局最大直径 + maxDiameter = Math.max(maxDiameter, myDiameter); + + traverse(root.left); + traverse(root.right); + } + + // 计算二叉树的最大深度 + int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + return 1 + Math.max(leftMax, rightMax); + } +} +``` + +这个解法是正确的,但是运行时间很长,原因也很明显,`traverse` 遍历每个节点的时候还会调用递归函数 `maxDepth`,而 `maxDepth` 是要遍历子树的所有节点的,所以最坏时间复杂度是 O(N^2)。 + +这就出现了刚才探讨的情况,**前序位置无法获取子树信息,所以只能让每个节点调用 `maxDepth` 函数去算子树的深度**。 + +那如何优化?我们应该把计算「直径」的逻辑放在后序位置,准确说应该是放在 `maxDepth` 的后序位置,因为 `maxDepth` 的后序位置是知道左右子树的最大深度的。 + +所以,稍微改一下代码逻辑即可得到更好的解法: + +```java +class Solution { + // 记录最大直径的长度 + int maxDiameter = 0; + + public int diameterOfBinaryTree(TreeNode root) { + maxDepth(root); + return maxDiameter; + } + + int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + // 后序位置,顺便计算最大直径 + int myDiameter = leftMax + rightMax; + maxDiameter = Math.max(maxDiameter, myDiameter); + + return 1 + Math.max(leftMax, rightMax); + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +这下时间复杂度只有 `maxDepth` 函数的 O(N) 了。 + +讲到这里,照应一下前文:遇到子树问题,首先想到的是给函数设置返回值,然后在后序位置做文章。 + +> [!NOTE] +> 思考题:请你思考一下,运用后序遍历的题目使用的是「遍历」的思路还是「分解问题」的思路? + + +> [!NOTE] +> 利用后序位置的题目,一般都使用「分解问题」的思路。因为当前节点接收并利用了子树返回的信息,这就意味着你把原问题分解成了当前节点 + 左右子树的子问题。 + +反过来,如果你写出了类似一开始的那种递归套递归的解法,大概率也需要反思是不是可以通过后序遍历优化了。 + +更多利用后序位置的习题参见 [手把手带你刷二叉树(后序篇)](https://labuladong.online/algo/data-structure/binary-tree-part3/)、[手把手带你刷二叉搜索树(后序篇)](https://labuladong.online/algo/data-structure/bst-part4/) 和 [【强化练习】利用后序位置解题](https://labuladong.online/algo/problem-set/binary-tree-post-order-i/)。 + + + + + + + +## 以树的视角看动归/回溯/DFS算法的区别和联系 + +前文我说动态规划/回溯算法就是二叉树算法两种不同思路的表现形式,相信能看到这里的读者应该也认可了我这个观点。但有细心的读者经常提问:你的思考方法让我豁然开朗,但你好像一直没讲过 DFS 算法? + +其实我在 [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) 中就是用的 DFS 算法,但我确实没有单独用一篇文章讲 DFS 算法,**因为 DFS 算法和回溯算法非常类似,只是在细节上有所区别**。 + +这个细节上的差别是什么呢?其实就是「做选择」和「撤销选择」到底在 for 循环外面还是里面的区别,DFS 算法在外面,回溯算法在里面。 + +为什么有这个区别?还是要结合着二叉树理解。这一部分我就把回溯算法、DFS 算法、动态规划三种经典的算法思想,以及它们和二叉树算法的联系和区别,用一句话来说明: + +> [!IMPORTANT] +> 动归/DFS/回溯算法都可以看做二叉树问题的扩展,只是它们的关注点不同: +> +> - 动态规划算法属于分解问题(分治)的思路,它的关注点在整棵「子树」。 +> - 回溯算法属于遍历的思路,它的关注点在节点间的「树枝」。 +> - DFS 算法属于遍历的思路,它的关注点在单个「节点」。 + +怎么理解?我分别举三个例子你就懂了。 + +### 例子一:分解问题的思想体现 + +**第一个例子**,给你一棵二叉树,请你用分解问题的思路写一个 `count` 函数,计算这棵二叉树共有多少个节点。代码很简单,上文都写过了: + +```java +// 定义:输入一棵二叉树,返回这棵二叉树的节点总数 +int count(TreeNode root) { + if (root == null) { + return 0; + } + // 当前节点关心的是两个子树的节点总数分别是多少 + // 因为用子问题的结果可以推导出原问题的结果 + int leftCount = count(root.left); + int rightCount = count(root.right); + // 后序位置,左右子树节点数加上自己就是整棵树的节点数 + return leftCount + rightCount + 1; +} +``` + +**你看,这就是动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」**。 + +你再看看具体的动态规划问题,比如 [动态规划框架套路详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 中举的斐波那契的例子,我们的关注点在一棵棵子树的返回值上: + +```java +int fib(int N) { + if (N == 1 || N == 2) return 1; + return fib(N - 1) + fib(N - 2); +} +``` + +![](https://labuladong.online/algo/images/dynamic-programming/2.jpg) + + + +### 例子二:回溯算法的思想体现 + +**第二个例子**,给你一棵二叉树,请你用遍历的思路写一个 `traverse` 函数,打印出遍历这棵二叉树的过程,你看下代码: + +```java +void traverse(TreeNode root) { + if (root == null) return; + printf("从节点 %s 进入节点 %s", root, root.left); + traverse(root.left); + printf("从节点 %s 回到节点 %s", root.left, root); + + printf("从节点 %s 进入节点 %s", root, root.right); + traverse(root.right); + printf("从节点 %s 回到节点 %s", root.right, root); +} +``` + +不难理解吧,好的,我们现在从二叉树进阶成多叉树,代码也是类似的: + +```java +// 多叉树节点 +class Node { + int val; + Node[] children; +} + +void traverse(Node root) { + if (root == null) return; + for (Node child : root.children) { + printf("从节点 %s 进入节点 %s", root, child); + traverse(child); + printf("从节点 %s 回到节点 %s", child, root); + } +} +``` + +这个多叉树的遍历框架就可以延伸出 [回溯算法框架套路详解](https://labuladong.online/algo/essential-technique/backtrack-framework/) 中的回溯算法框架: + + + + + +```java +// 回溯算法框架 +void backtrack(...) { + // base case + if (...) return; + + for (int i = 0; i < ...; i++) { + // 做选择 + ... + + // 进入下一层决策树 + backtrack(...); + + // 撤销刚才做的选择 + ... + } +} +``` + + + +**你看,这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,类比到二叉树上就是「树枝」**。 + +你再看看具体的回溯算法问题,比如 [回溯算法秒杀排列组合子集的九种题型](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) 中讲到的全排列,我们的关注点在一条条树枝上: + + + + + +```java +// 回溯算法核心部分代码 +void backtrack(int[] nums) { + // 回溯算法框架 + for (int i = 0; i < nums.length; i++) { + // 做选择 + used[i] = true; + track.addLast(nums[i]); + + // 进入下一层回溯树 + backtrack(nums); + + // 取消选择 + track.removeLast(); + used[i] = false; + } +} +``` + +![](https://labuladong.online/algo/images/permutation/2.jpeg) + + + +### 例子三:DFS 的思想体现 + +**第三个例子**,我给你一棵二叉树,请你写一个 `traverse` 函数,把这棵二叉树上的每个节点的值都加一。很简单吧,代码如下: + +```java +void traverse(TreeNode root) { + if (root == null) return; + // 遍历过的每个节点的值加一 + root.val++; + traverse(root.left); + traverse(root.right); +} +``` + +**你看,这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」**。 + +你再看看具体的 DFS 算法问题,比如 [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) 中讲的前几道题,我们的关注点是 `grid` 数组的每个格子(节点),我们要对遍历过的格子进行一些处理,所以我说是用 DFS 算法解决这几道题的: + + + + + +```java +// DFS 算法核心逻辑 +void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 0) { + return; + } + // 遍历过的每个格子标记为 0 + grid[i][j] = 0; + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); +} +``` + +![](https://labuladong.online/algo/images/island/5.jpg) + + + +好,请你仔细品一下上面三个简单的例子,是不是像我说的:动态规划关注整棵「子树」,回溯算法关注节点间的「树枝」,DFS 算法关注单个「节点」。 + +有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了,看下面两段代码: + +```java +// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面 +void dfs(Node root) { + if (root == null) return; + // 做选择 + print("enter node %s", root); + for (Node child : root.children) { + dfs(child); + } + // 撤销选择 + print("leave node %s", root); +} + +// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面 +void backtrack(Node root) { + if (root == null) return; + for (Node child : root.children) { + // 做选择 + print("I'm on the branch from %s to %s", root, child); + backtrack(child); + // 撤销选择 + print("I'll leave the branch from %s to %s", child, root); + } +} +``` + +看到了吧,你回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点? + +## 层序遍历 + +二叉树题型主要是用来培养递归思维的,而层序遍历属于迭代遍历,也比较简单,这里就过一下代码框架吧: + +```java +// 输入一棵二叉树的根节点,层序遍历这棵二叉树 +void levelTraverse(TreeNode root) { + if (root == null) return; + Queue q = new LinkedList<>(); + q.offer(root); + + // 从上到下遍历二叉树的每一层 + while (!q.isEmpty()) { + int sz = q.size(); + // 从左到右遍历每一层的每个节点 + for (int i = 0; i < sz; i++) { + TreeNode cur = q.poll(); + // 将下一层节点放入队列 + if (cur.left != null) { + q.offer(cur.left); + } + if (cur.right != null) { + q.offer(cur.right); + } + } + } +} +``` + +这里面 while 循环和 for 循环分管从上到下和从左到右的遍历: + +![](https://labuladong.online/algo/images/dijkstra/1.jpeg) + +前文 [BFS 算法框架](https://labuladong.online/algo/essential-technique/bfs-framework/) 就是从二叉树的层序遍历扩展出来的,常用于求无权图的**最短路径**问题。 + +当然这个框架还可以灵活修改,题目不需要记录层数(步数)时可以去掉上述框架中的 for 循环,比如前文 [Dijkstra 算法](https://labuladong.online/algo/data-structure/dijkstra/) 中计算加权图的最短路径问题,详细探讨了 BFS 算法的扩展。 + +值得一提的是,有些很明显需要用层序遍历技巧的二叉树的题目,也可以用递归遍历的方式去解决,而且技巧性会更强,非常考察你对前中后序的把控。 + +好了,本文已经够长了,围绕前中后序位置算是把二叉树题目里的各种套路给讲透了,真正能运用出来多少,就需要你亲自刷题实践和思考了。 + +希望大家能探索尽可能多的解法,只要参透二叉树这种基本数据结构的原理,那么就很容易在学习其他高级算法的道路上找到抓手,打通回路,形成闭环(手动狗头)。 + +最后,[二叉树递归专项练习](https://labuladong.online/algo/intro/binary-tree-practice/) 中会手把手带你运用本文所讲的技巧。 + +## 回答评论区的问题 + +关于层序遍历(以及其扩展出的 [BFS 算法框架](https://labuladong.online/algo/essential-technique/bfs-framework/)),我在最后多说几句。 + +如果你对二叉树足够熟悉,可以想到很多方式通过递归函数得到层序遍历结果,比如下面这种写法: + +```java +class Solution { + List> res = new ArrayList<>(); + + public List> levelTraverse(TreeNode root) { + if (root == null) { + return res; + } + // root 视为第 0 层 + traverse(root, 0); + return res; + } + + void traverse(TreeNode root, int depth) { + if (root == null) { + return; + } + // 前序位置,看看是否已经存储 depth 层的节点了 + if (res.size() <= depth) { + // 第一次进入 depth 层 + res.add(new LinkedList<>()); + } + // 前序位置,在 depth 层添加 root 节点的值 + res.get(depth).add(root.val); + traverse(root.left, depth + 1); + traverse(root.right, depth + 1); + } +} +``` + +这种思路从结果上说确实可以得到层序遍历结果,但其本质还是二叉树的前序遍历,或者说 DFS 的思路,而不是层序遍历,或者说 BFS 的思路。因为这个解法是依赖前序遍历自顶向下、自左向右的顺序特点得到了正确的结果。 + +**抽象点说,这个解法更像是从左到右的「列序遍历」,而不是自顶向下的「层序遍历」**。所以对于计算最小距离的场景,这个解法完全等同于 DFS 算法,没有 BFS 算法的性能的优势。 + +还有优秀读者评论了这样一种递归进行层序遍历的思路: + +```java +class Solution { + + List> res = new LinkedList<>(); + + public List> levelTraverse(TreeNode root) { + if (root == null) { + return res; + } + List nodes = new LinkedList<>(); + nodes.add(root); + traverse(nodes); + return res; + } + + void traverse(List curLevelNodes) { + // base case + if (curLevelNodes.isEmpty()) { + return; + } + // 前序位置,计算当前层的值和下一层的节点列表 + List nodeValues = new LinkedList<>(); + List nextLevelNodes = new LinkedList<>(); + for (TreeNode node : curLevelNodes) { + nodeValues.add(node.val); + if (node.left != null) { + nextLevelNodes.add(node.left); + } + if (node.right != null) { + nextLevelNodes.add(node.right); + } + } + // 前序位置添加结果,可以得到自顶向下的层序遍历 + res.add(nodeValues); + traverse(nextLevelNodes); + // 后序位置添加结果,可以得到自底向上的层序遍历结果 + // res.add(nodeValues); + } +} +``` + +这个 `traverse` 函数很像递归遍历单链表的函数,其实就是把二叉树的每一层抽象理解成单链表的一个节点进行遍历。 + +相较上一个递归解法,这个递归解法是自顶向下的「层序遍历」,更接近 BFS 的奥义,可以作为 BFS 算法的递归实现扩展一下思维。 + + + + + + + +
+
+引用本文的文章 + + - [Trie/字典树/前缀树代码实现](https://labuladong.online/algo/data-structure/trie-implement/) + - [【强化练习】BFS 经典习题 II](https://labuladong.online/algo/problem-set/bfs-ii/) + - [【强化练习】二叉搜索树经典例题 I](https://labuladong.online/algo/problem-set/bst1/) + - [【强化练习】二叉搜索树经典例题 II](https://labuladong.online/algo/problem-set/bst2/) + - [【强化练习】利用后序位置解题 I](https://labuladong.online/algo/problem-set/binary-tree-post-order-i/) + - [【强化练习】利用后序位置解题 II](https://labuladong.online/algo/problem-set/binary-tree-post-order-ii/) + - [【强化练习】利用后序位置解题 III](https://labuladong.online/algo/problem-set/binary-tree-post-order-iii/) + - [【强化练习】同时运用两种思维解题](https://labuladong.online/algo/problem-set/binary-tree-combine-two-view/) + - [【强化练习】哈希表更多习题](https://labuladong.online/algo/problem-set/hash-table/) + - [【强化练习】回溯算法经典习题 II](https://labuladong.online/algo/problem-set/backtrack-ii/) + - [【强化练习】回溯算法经典习题 III](https://labuladong.online/algo/problem-set/backtrack-iii/) + - [【强化练习】用「分解问题」思维解题 I](https://labuladong.online/algo/problem-set/binary-tree-divide-i/) + - [【强化练习】用「分解问题」思维解题 II](https://labuladong.online/algo/problem-set/binary-tree-divide-ii/) + - [【强化练习】用「遍历」思维解题 I](https://labuladong.online/algo/problem-set/binary-tree-traverse-i/) + - [【强化练习】用「遍历」思维解题 II](https://labuladong.online/algo/problem-set/binary-tree-traverse-ii/) + - [【强化练习】用「遍历」思维解题 III](https://labuladong.online/algo/problem-set/binary-tree-traverse-iii/) + - [【强化练习】运用层序遍历解题 I](https://labuladong.online/algo/problem-set/binary-tree-level-i/) + - [【强化练习】运用层序遍历解题 II](https://labuladong.online/algo/problem-set/binary-tree-level-ii/) + - [一个方法团灭 LeetCode 打家劫舍问题](https://labuladong.online/algo/dynamic-programming/house-robber/) + - [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) + - [二叉搜索树心法(后序篇)](https://labuladong.online/algo/data-structure/bst-part4/) + - [二叉树心法(后序篇)](https://labuladong.online/algo/data-structure/binary-tree-part3/) + - [二叉树心法(序列化篇)](https://labuladong.online/algo/data-structure/serialize-and-deserialize-binary-tree/) + - [二叉树心法(思路篇)](https://labuladong.online/algo/data-structure/binary-tree-part1/) + - [二叉树心法(构造篇)](https://labuladong.online/algo/data-structure/binary-tree-part2/) + - [二叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) + - [分治算法解题套路框架](https://labuladong.online/algo/essential-technique/divide-and-conquer/) + - [分治算法详解:运算优先级](https://labuladong.online/algo/fname.html?fname=分治算法) + - [动态规划和回溯算法的思维转换](https://labuladong.online/algo/dynamic-programming/word-break/) + - [动态规划穷举的两种视角](https://labuladong.online/algo/dynamic-programming/two-views-of-dp/) + - [回溯算法实践:集合划分](https://labuladong.online/algo/practice-in-action/partition-to-k-equal-sum-subsets/) + - [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) + - [回溯算法解题套路框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [拓展:归并排序详解及应用](https://labuladong.online/algo/practice-in-action/merge-sort/) + - [拓展:快速排序详解及应用](https://labuladong.online/algo/practice-in-action/quick-sort/) + - [拓展:最近公共祖先系列解题框架](https://labuladong.online/algo/practice-in-action/lowest-common-ancestor-summary/) + - [拓展:用栈模拟递归迭代遍历二叉树](https://labuladong.online/algo/data-structure/iterative-traversal-binary-tree/) + - [环检测及拓扑排序算法](https://labuladong.online/algo/data-structure/topological-sort/) + - [球盒模型:回溯算法穷举的两种视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) + - [算法学习和心流体验](https://labuladong.online/algo/fname.html?fname=心流) + - [经典动态规划:编辑距离](https://labuladong.online/algo/dynamic-programming/edit-distance/) + - [解答回溯算法/DFS算法的若干疑问](https://labuladong.online/algo/essential-technique/backtrack-vs-dfs/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [100. Same Tree](https://leetcode.com/problems/same-tree/?show=1) | [100. 相同的树](https://leetcode.cn/problems/same-tree/?show=1) | 🟢 | +| [1008. Construct Binary Search Tree from Preorder Traversal](https://leetcode.com/problems/construct-binary-search-tree-from-preorder-traversal/?show=1) | [1008. 前序遍历构造二叉搜索树](https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/?show=1) | 🟠 | +| [101. Symmetric Tree](https://leetcode.com/problems/symmetric-tree/?show=1) | [101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/?show=1) | 🟢 | +| [1022. Sum of Root To Leaf Binary Numbers](https://leetcode.com/problems/sum-of-root-to-leaf-binary-numbers/?show=1) | [1022. 从根到叶的二进制数之和](https://leetcode.cn/problems/sum-of-root-to-leaf-binary-numbers/?show=1) | 🟢 | +| [1026. Maximum Difference Between Node and Ancestor](https://leetcode.com/problems/maximum-difference-between-node-and-ancestor/?show=1) | [1026. 节点与其祖先之间的最大差值](https://leetcode.cn/problems/maximum-difference-between-node-and-ancestor/?show=1) | 🟠 | +| [108. Convert Sorted Array to Binary Search Tree](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/?show=1) | [108. 将有序数组转换为二叉搜索树](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/?show=1) | 🟢 | +| [1080. Insufficient Nodes in Root to Leaf Paths](https://leetcode.com/problems/insufficient-nodes-in-root-to-leaf-paths/?show=1) | [1080. 根到叶路径上的不足节点](https://leetcode.cn/problems/insufficient-nodes-in-root-to-leaf-paths/?show=1) | 🟠 | +| [110. Balanced Binary Tree](https://leetcode.com/problems/balanced-binary-tree/?show=1) | [110. 平衡二叉树](https://leetcode.cn/problems/balanced-binary-tree/?show=1) | 🟢 | +| [111. Minimum Depth of Binary Tree](https://leetcode.com/problems/minimum-depth-of-binary-tree/?show=1) | [111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/?show=1) | 🟢 | +| [1110. Delete Nodes And Return Forest](https://leetcode.com/problems/delete-nodes-and-return-forest/?show=1) | [1110. 删点成林](https://leetcode.cn/problems/delete-nodes-and-return-forest/?show=1) | 🟠 | +| [1120. Maximum Average Subtree](https://leetcode.com/problems/maximum-average-subtree/?show=1)🔒 | [1120. 子树的最大平均值](https://leetcode.cn/problems/maximum-average-subtree/?show=1)🔒 | 🟠 | +| [113. Path Sum II](https://leetcode.com/problems/path-sum-ii/?show=1) | [113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/?show=1) | 🟠 | +| [114. Flatten Binary Tree to Linked List](https://leetcode.com/problems/flatten-binary-tree-to-linked-list/?show=1) | [114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/?show=1) | 🟠 | +| [116. Populating Next Right Pointers in Each Node](https://leetcode.com/problems/populating-next-right-pointers-in-each-node/?show=1) | [116. 填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/?show=1) | 🟠 | +| [124. Binary Tree Maximum Path Sum](https://leetcode.com/problems/binary-tree-maximum-path-sum/?show=1) | [124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/?show=1) | 🔴 | +| [1245. Tree Diameter](https://leetcode.com/problems/tree-diameter/?show=1)🔒 | [1245. 树的直径](https://leetcode.cn/problems/tree-diameter/?show=1)🔒 | 🟠 | +| [1261. Find Elements in a Contaminated Binary Tree](https://leetcode.com/problems/find-elements-in-a-contaminated-binary-tree/?show=1) | [1261. 在受污染的二叉树中查找元素](https://leetcode.cn/problems/find-elements-in-a-contaminated-binary-tree/?show=1) | 🟠 | +| [129. Sum Root to Leaf Numbers](https://leetcode.com/problems/sum-root-to-leaf-numbers/?show=1) | [129. 求根节点到叶节点数字之和](https://leetcode.cn/problems/sum-root-to-leaf-numbers/?show=1) | 🟠 | +| [1315. Sum of Nodes with Even-Valued Grandparent](https://leetcode.com/problems/sum-of-nodes-with-even-valued-grandparent/?show=1) | [1315. 祖父节点值为偶数的节点和](https://leetcode.cn/problems/sum-of-nodes-with-even-valued-grandparent/?show=1) | 🟠 | +| [1325. Delete Leaves With a Given Value](https://leetcode.com/problems/delete-leaves-with-a-given-value/?show=1) | [1325. 删除给定值的叶子节点](https://leetcode.cn/problems/delete-leaves-with-a-given-value/?show=1) | 🟠 | +| [1339. Maximum Product of Splitted Binary Tree](https://leetcode.com/problems/maximum-product-of-splitted-binary-tree/?show=1) | [1339. 分裂二叉树的最大乘积](https://leetcode.cn/problems/maximum-product-of-splitted-binary-tree/?show=1) | 🟠 | +| [1367. Linked List in Binary Tree](https://leetcode.com/problems/linked-list-in-binary-tree/?show=1) | [1367. 二叉树中的链表](https://leetcode.cn/problems/linked-list-in-binary-tree/?show=1) | 🟠 | +| [1372. Longest ZigZag Path in a Binary Tree](https://leetcode.com/problems/longest-zigzag-path-in-a-binary-tree/?show=1) | [1372. 二叉树中的最长交错路径](https://leetcode.cn/problems/longest-zigzag-path-in-a-binary-tree/?show=1) | 🟠 | +| [1373. Maximum Sum BST in Binary Tree](https://leetcode.com/problems/maximum-sum-bst-in-binary-tree/?show=1) | [1373. 二叉搜索子树的最大键值和](https://leetcode.cn/problems/maximum-sum-bst-in-binary-tree/?show=1) | 🔴 | +| [1376. Time Needed to Inform All Employees](https://leetcode.com/problems/time-needed-to-inform-all-employees/?show=1) | [1376. 通知所有员工所需的时间](https://leetcode.cn/problems/time-needed-to-inform-all-employees/?show=1) | 🟠 | +| [1379. Find a Corresponding Node of a Binary Tree in a Clone of That Tree](https://leetcode.com/problems/find-a-corresponding-node-of-a-binary-tree-in-a-clone-of-that-tree/?show=1) | [1379. 找出克隆二叉树中的相同节点](https://leetcode.cn/problems/find-a-corresponding-node-of-a-binary-tree-in-a-clone-of-that-tree/?show=1) | 🟢 | +| [1430. Check If a String Is a Valid Sequence from Root to Leaves Path in a Binary Tree](https://leetcode.com/problems/check-if-a-string-is-a-valid-sequence-from-root-to-leaves-path-in-a-binary-tree/?show=1)🔒 | [1430. 判断给定的序列是否是二叉树从根到叶的路径](https://leetcode.cn/problems/check-if-a-string-is-a-valid-sequence-from-root-to-leaves-path-in-a-binary-tree/?show=1)🔒 | 🟠 | +| [1443. Minimum Time to Collect All Apples in a Tree](https://leetcode.com/problems/minimum-time-to-collect-all-apples-in-a-tree/?show=1) | [1443. 收集树上所有苹果的最少时间](https://leetcode.cn/problems/minimum-time-to-collect-all-apples-in-a-tree/?show=1) | 🟠 | +| [1448. Count Good Nodes in Binary Tree](https://leetcode.com/problems/count-good-nodes-in-binary-tree/?show=1) | [1448. 统计二叉树中好节点的数目](https://leetcode.cn/problems/count-good-nodes-in-binary-tree/?show=1) | 🟠 | +| [145. Binary Tree Postorder Traversal](https://leetcode.com/problems/binary-tree-postorder-traversal/?show=1) | [145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/?show=1) | 🟢 | +| [1457. Pseudo-Palindromic Paths in a Binary Tree](https://leetcode.com/problems/pseudo-palindromic-paths-in-a-binary-tree/?show=1) | [1457. 二叉树中的伪回文路径](https://leetcode.cn/problems/pseudo-palindromic-paths-in-a-binary-tree/?show=1) | 🟠 | +| [1469. Find All The Lonely Nodes](https://leetcode.com/problems/find-all-the-lonely-nodes/?show=1)🔒 | [1469. 寻找所有的独生节点](https://leetcode.cn/problems/find-all-the-lonely-nodes/?show=1)🔒 | 🟢 | +| [1485. Clone Binary Tree With Random Pointer](https://leetcode.com/problems/clone-binary-tree-with-random-pointer/?show=1)🔒 | [1485. 克隆含随机指针的二叉树](https://leetcode.cn/problems/clone-binary-tree-with-random-pointer/?show=1)🔒 | 🟠 | +| [1490. Clone N-ary Tree](https://leetcode.com/problems/clone-n-ary-tree/?show=1)🔒 | [1490. 克隆 N 叉树](https://leetcode.cn/problems/clone-n-ary-tree/?show=1)🔒 | 🟠 | +| [1593. Split a String Into the Max Number of Unique Substrings](https://leetcode.com/problems/split-a-string-into-the-max-number-of-unique-substrings/?show=1) | [1593. 拆分字符串使唯一子字符串的数目最大](https://leetcode.cn/problems/split-a-string-into-the-max-number-of-unique-substrings/?show=1) | 🟠 | +| [1602. Find Nearest Right Node in Binary Tree](https://leetcode.com/problems/find-nearest-right-node-in-binary-tree/?show=1)🔒 | [1602. 找到二叉树中最近的右侧节点](https://leetcode.cn/problems/find-nearest-right-node-in-binary-tree/?show=1)🔒 | 🟠 | +| [1612. Check If Two Expression Trees are Equivalent](https://leetcode.com/problems/check-if-two-expression-trees-are-equivalent/?show=1)🔒 | [1612. 检查两棵二叉表达式树是否等价](https://leetcode.cn/problems/check-if-two-expression-trees-are-equivalent/?show=1)🔒 | 🟠 | +| [1740. Find Distance in a Binary Tree](https://leetcode.com/problems/find-distance-in-a-binary-tree/?show=1)🔒 | [1740. 找到二叉树中的距离](https://leetcode.cn/problems/find-distance-in-a-binary-tree/?show=1)🔒 | 🟠 | +| [2049. Count Nodes With the Highest Score](https://leetcode.com/problems/count-nodes-with-the-highest-score/?show=1) | [2049. 统计最高分的节点数目](https://leetcode.cn/problems/count-nodes-with-the-highest-score/?show=1) | 🟠 | +| [2096. Step-By-Step Directions From a Binary Tree Node to Another](https://leetcode.com/problems/step-by-step-directions-from-a-binary-tree-node-to-another/?show=1) | [2096. 从二叉树一个节点到另一个节点每一步的方向](https://leetcode.cn/problems/step-by-step-directions-from-a-binary-tree-node-to-another/?show=1) | 🟠 | +| [226. Invert Binary Tree](https://leetcode.com/problems/invert-binary-tree/?show=1) | [226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/?show=1) | 🟢 | +| [250. Count Univalue Subtrees](https://leetcode.com/problems/count-univalue-subtrees/?show=1)🔒 | [250. 统计同值子树](https://leetcode.cn/problems/count-univalue-subtrees/?show=1)🔒 | 🟠 | +| [254. Factor Combinations](https://leetcode.com/problems/factor-combinations/?show=1)🔒 | [254. 因子的组合](https://leetcode.cn/problems/factor-combinations/?show=1)🔒 | 🟠 | +| [257. Binary Tree Paths](https://leetcode.com/problems/binary-tree-paths/?show=1) | [257. 二叉树的所有路径](https://leetcode.cn/problems/binary-tree-paths/?show=1) | 🟢 | +| [267. Palindrome Permutation II](https://leetcode.com/problems/palindrome-permutation-ii/?show=1)🔒 | [267. 回文排列 II](https://leetcode.cn/problems/palindrome-permutation-ii/?show=1)🔒 | 🟠 | +| [270. Closest Binary Search Tree Value](https://leetcode.com/problems/closest-binary-search-tree-value/?show=1)🔒 | [270. 最接近的二叉搜索树值](https://leetcode.cn/problems/closest-binary-search-tree-value/?show=1)🔒 | 🟢 | +| [294. Flip Game II](https://leetcode.com/problems/flip-game-ii/?show=1)🔒 | [294. 翻转游戏 II](https://leetcode.cn/problems/flip-game-ii/?show=1)🔒 | 🟠 | +| [298. Binary Tree Longest Consecutive Sequence](https://leetcode.com/problems/binary-tree-longest-consecutive-sequence/?show=1)🔒 | [298. 二叉树最长连续序列](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence/?show=1)🔒 | 🟠 | +| [332. Reconstruct Itinerary](https://leetcode.com/problems/reconstruct-itinerary/?show=1) | [332. 重新安排行程](https://leetcode.cn/problems/reconstruct-itinerary/?show=1) | 🔴 | +| [333. Largest BST Subtree](https://leetcode.com/problems/largest-bst-subtree/?show=1)🔒 | [333. 最大 BST 子树](https://leetcode.cn/problems/largest-bst-subtree/?show=1)🔒 | 🟠 | +| [339. Nested List Weight Sum](https://leetcode.com/problems/nested-list-weight-sum/?show=1)🔒 | [339. 嵌套列表权重和](https://leetcode.cn/problems/nested-list-weight-sum/?show=1)🔒 | 🟠 | +| [366. Find Leaves of Binary Tree](https://leetcode.com/problems/find-leaves-of-binary-tree/?show=1)🔒 | [366. 寻找二叉树的叶子节点](https://leetcode.cn/problems/find-leaves-of-binary-tree/?show=1)🔒 | 🟠 | +| [386. Lexicographical Numbers](https://leetcode.com/problems/lexicographical-numbers/?show=1) | [386. 字典序排数](https://leetcode.cn/problems/lexicographical-numbers/?show=1) | 🟠 | +| [404. Sum of Left Leaves](https://leetcode.com/problems/sum-of-left-leaves/?show=1) | [404. 左叶子之和](https://leetcode.cn/problems/sum-of-left-leaves/?show=1) | 🟢 | +| [426. Convert Binary Search Tree to Sorted Doubly Linked List](https://leetcode.com/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/?show=1)🔒 | [426. 将二叉搜索树转化为排序的双向链表](https://leetcode.cn/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/?show=1)🔒 | 🟠 | +| [437. Path Sum III](https://leetcode.com/problems/path-sum-iii/?show=1) | [437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/?show=1) | 🟠 | +| [501. Find Mode in Binary Search Tree](https://leetcode.com/problems/find-mode-in-binary-search-tree/?show=1) | [501. 二叉搜索树中的众数](https://leetcode.cn/problems/find-mode-in-binary-search-tree/?show=1) | 🟢 | +| [508. Most Frequent Subtree Sum](https://leetcode.com/problems/most-frequent-subtree-sum/?show=1) | [508. 出现次数最多的子树元素和](https://leetcode.cn/problems/most-frequent-subtree-sum/?show=1) | 🟠 | +| [513. Find Bottom Left Tree Value](https://leetcode.com/problems/find-bottom-left-tree-value/?show=1) | [513. 找树左下角的值](https://leetcode.cn/problems/find-bottom-left-tree-value/?show=1) | 🟠 | +| [515. Find Largest Value in Each Tree Row](https://leetcode.com/problems/find-largest-value-in-each-tree-row/?show=1) | [515. 在每个树行中找最大值](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/?show=1) | 🟠 | +| [530. Minimum Absolute Difference in BST](https://leetcode.com/problems/minimum-absolute-difference-in-bst/?show=1) | [530. 二叉搜索树的最小绝对差](https://leetcode.cn/problems/minimum-absolute-difference-in-bst/?show=1) | 🟢 | +| [538. Convert BST to Greater Tree](https://leetcode.com/problems/convert-bst-to-greater-tree/?show=1) | [538. 把二叉搜索树转换为累加树](https://leetcode.cn/problems/convert-bst-to-greater-tree/?show=1) | 🟠 | +| [549. Binary Tree Longest Consecutive Sequence II](https://leetcode.com/problems/binary-tree-longest-consecutive-sequence-ii/?show=1)🔒 | [549. 二叉树中最长的连续序列](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence-ii/?show=1)🔒 | 🟠 | +| [559. Maximum Depth of N-ary Tree](https://leetcode.com/problems/maximum-depth-of-n-ary-tree/?show=1) | [559. N 叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-n-ary-tree/?show=1) | 🟢 | +| [563. Binary Tree Tilt](https://leetcode.com/problems/binary-tree-tilt/?show=1) | [563. 二叉树的坡度](https://leetcode.cn/problems/binary-tree-tilt/?show=1) | 🟢 | +| [572. Subtree of Another Tree](https://leetcode.com/problems/subtree-of-another-tree/?show=1) | [572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/?show=1) | 🟢 | +| [582. Kill Process](https://leetcode.com/problems/kill-process/?show=1)🔒 | [582. 杀掉进程](https://leetcode.cn/problems/kill-process/?show=1)🔒 | 🟠 | +| [606. Construct String from Binary Tree](https://leetcode.com/problems/construct-string-from-binary-tree/?show=1) | [606. 根据二叉树创建字符串](https://leetcode.cn/problems/construct-string-from-binary-tree/?show=1) | 🟢 | +| [617. Merge Two Binary Trees](https://leetcode.com/problems/merge-two-binary-trees/?show=1) | [617. 合并二叉树](https://leetcode.cn/problems/merge-two-binary-trees/?show=1) | 🟢 | +| [623. Add One Row to Tree](https://leetcode.com/problems/add-one-row-to-tree/?show=1) | [623. 在二叉树中增加一行](https://leetcode.cn/problems/add-one-row-to-tree/?show=1) | 🟠 | +| [654. Maximum Binary Tree](https://leetcode.com/problems/maximum-binary-tree/?show=1) | [654. 最大二叉树](https://leetcode.cn/problems/maximum-binary-tree/?show=1) | 🟠 | +| [663. Equal Tree Partition](https://leetcode.com/problems/equal-tree-partition/?show=1)🔒 | [663. 均匀树划分](https://leetcode.cn/problems/equal-tree-partition/?show=1)🔒 | 🟠 | +| [666. Path Sum IV](https://leetcode.com/problems/path-sum-iv/?show=1)🔒 | [666. 路径总和 IV](https://leetcode.cn/problems/path-sum-iv/?show=1)🔒 | 🟠 | +| [669. Trim a Binary Search Tree](https://leetcode.com/problems/trim-a-binary-search-tree/?show=1) | [669. 修剪二叉搜索树](https://leetcode.cn/problems/trim-a-binary-search-tree/?show=1) | 🟠 | +| [671. Second Minimum Node In a Binary Tree](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/?show=1) | [671. 二叉树中第二小的节点](https://leetcode.cn/problems/second-minimum-node-in-a-binary-tree/?show=1) | 🟢 | +| [687. Longest Univalue Path](https://leetcode.com/problems/longest-univalue-path/?show=1) | [687. 最长同值路径](https://leetcode.cn/problems/longest-univalue-path/?show=1) | 🟠 | +| [776. Split BST](https://leetcode.com/problems/split-bst/?show=1)🔒 | [776. 拆分二叉搜索树](https://leetcode.cn/problems/split-bst/?show=1)🔒 | 🟠 | +| [865. Smallest Subtree with all the Deepest Nodes](https://leetcode.com/problems/smallest-subtree-with-all-the-deepest-nodes/?show=1) | [865. 具有所有最深节点的最小子树](https://leetcode.cn/problems/smallest-subtree-with-all-the-deepest-nodes/?show=1) | 🟠 | +| [894. All Possible Full Binary Trees](https://leetcode.com/problems/all-possible-full-binary-trees/?show=1) | [894. 所有可能的真二叉树](https://leetcode.cn/problems/all-possible-full-binary-trees/?show=1) | 🟠 | +| [897. Increasing Order Search Tree](https://leetcode.com/problems/increasing-order-search-tree/?show=1) | [897. 递增顺序搜索树](https://leetcode.cn/problems/increasing-order-search-tree/?show=1) | 🟢 | +| [938. Range Sum of BST](https://leetcode.com/problems/range-sum-of-bst/?show=1) | [938. 二叉搜索树的范围和](https://leetcode.cn/problems/range-sum-of-bst/?show=1) | 🟢 | +| [951. Flip Equivalent Binary Trees](https://leetcode.com/problems/flip-equivalent-binary-trees/?show=1) | [951. 翻转等价二叉树](https://leetcode.cn/problems/flip-equivalent-binary-trees/?show=1) | 🟠 | +| [965. Univalued Binary Tree](https://leetcode.com/problems/univalued-binary-tree/?show=1) | [965. 单值二叉树](https://leetcode.cn/problems/univalued-binary-tree/?show=1) | 🟢 | +| [968. Binary Tree Cameras](https://leetcode.com/problems/binary-tree-cameras/?show=1) | [968. 监控二叉树](https://leetcode.cn/problems/binary-tree-cameras/?show=1) | 🔴 | +| [971. Flip Binary Tree To Match Preorder Traversal](https://leetcode.com/problems/flip-binary-tree-to-match-preorder-traversal/?show=1) | [971. 翻转二叉树以匹配先序遍历](https://leetcode.cn/problems/flip-binary-tree-to-match-preorder-traversal/?show=1) | 🟠 | +| [979. Distribute Coins in Binary Tree](https://leetcode.com/problems/distribute-coins-in-binary-tree/?show=1) | [979. 在二叉树中分配硬币](https://leetcode.cn/problems/distribute-coins-in-binary-tree/?show=1) | 🟠 | +| [987. Vertical Order Traversal of a Binary Tree](https://leetcode.com/problems/vertical-order-traversal-of-a-binary-tree/?show=1) | [987. 二叉树的垂序遍历](https://leetcode.cn/problems/vertical-order-traversal-of-a-binary-tree/?show=1) | 🔴 | +| [988. Smallest String Starting From Leaf](https://leetcode.com/problems/smallest-string-starting-from-leaf/?show=1) | [988. 从叶结点开始的最小字符串](https://leetcode.cn/problems/smallest-string-starting-from-leaf/?show=1) | 🟠 | +| [99. Recover Binary Search Tree](https://leetcode.com/problems/recover-binary-search-tree/?show=1) | [99. 恢复二叉搜索树](https://leetcode.cn/problems/recover-binary-search-tree/?show=1) | 🟠 | +| [993. Cousins in Binary Tree](https://leetcode.com/problems/cousins-in-binary-tree/?show=1) | [993. 二叉树的堂兄弟节点](https://leetcode.cn/problems/cousins-in-binary-tree/?show=1) | 🟢 | +| [998. Maximum Binary Tree II](https://leetcode.com/problems/maximum-binary-tree-ii/?show=1) | [998. 最大二叉树 II](https://leetcode.cn/problems/maximum-binary-tree-ii/?show=1) | 🟠 | +| - | [剑指 Offer 06. 从尾到头打印链表](https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 26. 树的子结构](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 27. 二叉树的镜像](https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 28. 对称的二叉树](https://leetcode.cn/problems/dui-cheng-de-er-cha-shu-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 33. 二叉搜索树的后序遍历序列](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 34. 二叉树中和为某一值的路径](https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 36. 二叉搜索树与双向链表](https://leetcode.cn/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 55 - I. 二叉树的深度](https://leetcode.cn/problems/er-cha-shu-de-shen-du-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 55 - II. 平衡二叉树](https://leetcode.cn/problems/ping-heng-er-cha-shu-lcof/?show=1) | 🟢 | +| - | [剑指 Offer II 044. 二叉树每层的最大值](https://leetcode.cn/problems/hPov7L/?show=1) | 🟠 | +| - | [剑指 Offer II 045. 二叉树最底层最左边的值](https://leetcode.cn/problems/LwUNpT/?show=1) | 🟠 | +| - | [剑指 Offer II 049. 从根节点到叶节点的路径数字之和](https://leetcode.cn/problems/3Etpl5/?show=1) | 🟠 | +| - | [剑指 Offer II 050. 向下的路径节点之和](https://leetcode.cn/problems/6eUYwP/?show=1) | 🟠 | +| - | [剑指 Offer II 051. 节点之和最大的路径](https://leetcode.cn/problems/jC7MId/?show=1) | 🔴 | +| - | [剑指 Offer II 052. 展平二叉搜索树](https://leetcode.cn/problems/NYBBNL/?show=1) | 🟢 | +| - | [剑指 Offer II 054. 所有大于等于节点的值之和](https://leetcode.cn/problems/w6cpku/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2271.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2271.md" new file mode 100644 index 0000000000..38d8d5b090 --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2271.md" @@ -0,0 +1,458 @@ +# 二叉树心法(思路篇) + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [114. Flatten Binary Tree to Linked List](https://leetcode.com/problems/flatten-binary-tree-to-linked-list/) | [114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/) | 🟠 | +| [116. Populating Next Right Pointers in Each Node](https://leetcode.com/problems/populating-next-right-pointers-in-each-node/) | [116. 填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) | 🟠 | +| [226. Invert Binary Tree](https://leetcode.com/problems/invert-binary-tree/) | [226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树结构基础](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) +> - [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) +> - [二叉树心法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + +> tip:本文有视频版:[二叉树/递归的框架思维(纲领篇)](https://www.bilibili.com/video/BV1nG411x77H/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +本文承接 [二叉树心法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/),先复述一下前文总结的二叉树解题总纲: + +> [!NOTE] +> 二叉树解题的思维模式分两类: +> +> **1、是否可以通过遍历一遍二叉树得到答案**?如果可以,用一个 `traverse` 函数配合外部变量来实现,这叫「遍历」的思维模式。 +> +> **2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案**?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。 +> +> 无论使用哪种思维模式,你都需要思考: +> +> **如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做**?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。 + +本文就以几道比较简单的题目为例,带你实践运用这几条总纲,理解「遍历」的思维和「分解问题」的思维有何区别和联系。 + + + + + + + +## 第一题、翻转二叉树 + +我们先从简单的题开始,看看力扣第 226 题「翻转二叉树」,输入一个二叉树根节点 `root`,让你把整棵树镜像翻转,比如输入的二叉树如下: + +``` + 4 + / \ + 2 7 + / \ / \ +1 3 6 9 +``` + +算法原地翻转二叉树,使得以 `root` 为根的树变成: + +``` + 4 + / \ + 7 2 + / \ / \ +9 6 3 1 +``` + +不难发现,只要把二叉树上的每一个节点的左右子节点进行交换,最后的结果就是完全翻转之后的二叉树。 + +那么现在开始在心中默念二叉树解题总纲: + +**1、这题能不能用「遍历」的思维模式解决**? + +可以,我写一个 `traverse` 函数遍历每个节点,让每个节点的左右子节点颠倒过来就行了。 + +单独抽出一个节点,需要让它做什么?让它把自己的左右子节点交换一下。 + +需要在什么时候做?好像前中后序位置都可以。 + +综上,可以写出如下解法代码: + +```java +class Solution { + // 主函数 + public TreeNode invertTree(TreeNode root) { + // 遍历二叉树,交换每个节点的子节点 + traverse(root); + return root; + } + + // 二叉树遍历函数 + void traverse(TreeNode root) { + if (root == null) { + return; + } + + // *** 前序位置 *** + // 每一个节点需要做的事就是交换它的左右子节点 + TreeNode tmp = root.left; + root.left = root.right; + root.right = tmp; + + // 遍历框架,去遍历左右子树的节点 + traverse(root.left); + traverse(root.right); + } +} +``` + + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + +你把前序位置的代码移到后序位置也可以,但是直接移到中序位置是不行的,需要稍作修改,这应该很容易看出来吧,我就不说了。 + +按理说,这道题已经解决了,不过为了对比,我们再继续思考下去。 + +**2、这题能不能用「分解问题」的思维模式解决**? + +我们尝试给 `invertTree` 函数赋予一个定义: + +```java +// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 +TreeNode invertTree(TreeNode root); +``` + +然后思考,对于某一个二叉树节点 `x` 执行 `invertTree(x)`,你能利用这个递归函数的定义做点啥? + +我可以用 `invertTree(x.left)` 先把 `x` 的左子树翻转,再用 `invertTree(x.right)` 把 `x` 的右子树翻转,最后把 `x` 的左右子树交换,这恰好完成了以 `x` 为根的整棵二叉树的翻转,即完成了 `invertTree(x)` 的定义。 + +直接写出解法代码: + +```java +class Solution { + // 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 + public TreeNode invertTree(TreeNode root) { + if (root == null) { + return null; + } + // 利用函数定义,先翻转左右子树 + TreeNode left = invertTree(root.left); + TreeNode right = invertTree(root.right); + + // 然后交换左右子节点 + root.left = right; + root.right = left; + + // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root + return root; + } +} +``` + + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + +这种「分解问题」的思路,核心在于你要给递归函数一个合适的定义,然后用函数的定义来解释你的代码;如果你的逻辑成功自恰,那么说明你这个算法是正确的。 + +好了,这道题就分析到这,「遍历」和「分解问题」的思路都可以解决,看下一道题。 + +## 第二题、填充节点的右侧指针 + +这是力扣第 116 题「填充每个二叉树节点的右侧指针」,看下题目: + + + + + + + +```java +// 函数签名 +Node connect(Node root); +``` + +题目的意思就是把二叉树的每一层节点都用 `next` 指针连接起来: + +![](https://labuladong.online/algo/images/binary-tree-i/1.png) + +而且题目说了,输入是一棵「完美二叉树」,形象地说整棵二叉树是一个正三角形,除了最右侧的节点 `next` 指针会指向 `null`,其他节点的右侧一定有相邻的节点。 + +这道题怎么做呢?来默念二叉树解题总纲: + +**1、这题能不能用「遍历」的思维模式解决**? + +很显然,一定可以。 + +每个节点要做的事也很简单,把自己的 `next` 指针指向右侧节点就行了。 + +也许你会模仿上一道题,直接写出如下代码: + +```java +// 二叉树遍历函数 +void traverse(Node root) { + if (root == null || root.left == null) { + return; + } + // 把左子节点的 next 指针指向右子节点 + root.left.next = root.right; + + traverse(root.left); + traverse(root.right); +} +``` + +但是,这段代码其实有很大问题,因为它只能把相同父节点的两个节点穿起来,再看看这张图: + +![](https://labuladong.online/algo/images/binary-tree-i/1.png) + +节点 5 和节点 6 不属于同一个父节点,那么按照这段代码的逻辑,它俩就没办法被穿起来,这是不符合题意的,但是问题出在哪里? + +**传统的 `traverse` 函数是遍历二叉树的所有节点,但现在我们想遍历的其实是两个相邻节点之间的「空隙」**。 + +所以我们可以在二叉树的基础上进行抽象,你把图中的每一个方框看做一个节点: + +![](https://labuladong.online/algo/images/binary-tree-i/3.png) + +**这样,一棵二叉树被抽象成了一棵三叉树,三叉树上的每个节点就是原先二叉树的两个相邻节点**。 + +现在,我们只要实现一个 `traverse` 函数来遍历这棵三叉树,每个「三叉树节点」需要做的事就是把自己内部的两个二叉树节点穿起来: + +```java +class Solution { + // 主函数 + public Node connect(Node root) { + if (root == null) return null; + // 遍历「三叉树」,连接相邻节点 + traverse(root.left, root.right); + return root; + } + + // 三叉树遍历框架 + void traverse(Node node1, Node node2) { + if (node1 == null || node2 == null) { + return; + } + // *** 前序位置 *** + // 将传入的两个节点穿起来 + node1.next = node2; + + // 连接相同父节点的两个子节点 + traverse(node1.left, node1.right); + traverse(node2.left, node2.right); + // 连接跨越父节点的两个子节点 + traverse(node1.right, node2.left); + } +} +``` + +这样,`traverse` 函数遍历整棵「三叉树」,将所有相邻节的二叉树节点都连接起来,也就避免了我们之前出现的问题,把这道题完美解决。 + +**2、这题能不能用「分解问题」的思维模式解决**? + +嗯,好像没有什么特别好的思路,所以这道题无法使用「分解问题」的思维来解决。 + +## 第三题、将二叉树展开为链表 + +这是力扣第 114 题「将二叉树展开为链表」,看下题目: + + + + + + + +```java +// 函数签名如下 +void flatten(TreeNode root); +``` + +**1、这题能不能用「遍历」的思维模式解决**? + +乍一看感觉是可以的:对整棵树进行前序遍历,一边遍历一边构造出一条「链表」就行了: + +```java +// 虚拟头节点,dummy.right 就是结果 +TreeNode dummy = new TreeNode(-1); +// 用来构建链表的指针 +TreeNode p = dummy; + +void traverse(TreeNode root) { + if (root == null) { + return; + } + // 前序位置 + p.right = new TreeNode(root.val); + p = p.right; + + traverse(root.left); + traverse(root.right); +} +``` + +但是注意 `flatten` 函数的签名,返回类型为 `void`,也就是说题目希望我们在原地把二叉树拉平成链表。 + +这样一来,没办法通过简单的二叉树遍历来解决这道题了。 + +**2、这题能不能用「分解问题」的思维模式解决**? + +我们尝试给出 `flatten` 函数的定义: + +```java +// 定义:输入节点 root,然后 root 为根的二叉树就会被拉平为一条链表 +void flatten(TreeNode root); +``` + +有了这个函数定义,如何按题目要求把一棵树拉平成一条链表? + +对于一个节点 `x`,可以执行以下流程: + +1、先利用 `flatten(x.left)` 和 `flatten(x.right)` 将 `x` 的左右子树拉平。 + +2、将 `x` 的右子树接到左子树下方,然后将整个左子树作为右子树。 + +![](https://labuladong.online/algo/images/binary-tree-i/2.jpeg) + +这样,以 `x` 为根的整棵二叉树就被拉平了,恰好完成了 `flatten(x)` 的定义。 + +直接看代码实现: + +```java +class Solution { + // 定义:将以 root 为根的树拉平为链表 + public void flatten(TreeNode root) { + // base case + if (root == null) return; + + // 利用定义,把左右子树拉平 + flatten(root.left); + flatten(root.right); + + // *** 后序遍历位置 *** + // 1、左右子树已经被拉平成一条链表 + TreeNode left = root.left; + TreeNode right = root.right; + + // 2、将左子树作为右子树 + root.left = null; + root.right = left; + + // 3、将原先的右子树接到当前右子树的末端 + TreeNode p = root; + while (p.right != null) { + p = p.right; + } + p.right = right; + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +你看,这就是递归的魅力,你说 `flatten` 函数是怎么把左右子树拉平的? + +不容易说清楚,但是只要知道 `flatten` 的定义如此并利用这个定义,让每一个节点做它该做的事情,然后 `flatten` 函数就会按照定义工作。 + +至此,这道题也解决了,我们前文 [k个一组翻转链表](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/) 的递归思路和本题也有一些类似。 + +最后,首尾呼应,再次默写二叉树解题总纲。 + +二叉树解题的思维模式分两类: + +**1、是否可以通过遍历一遍二叉树得到答案**?如果可以,用一个 `traverse` 函数配合外部变量来实现,这叫「遍历」的思维模式。 + +**2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案**?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。 + +无论使用哪种思维模式,你都需要思考: + +**如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做**?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。 + +希望你能仔细体会,并运用到所有二叉树题目上。 + +本文就到这里,更多经典的二叉树习题以及递归思维的训练,请参见二叉树章节中的 [递归专项练习](https://labuladong.online/algo/intro/binary-tree-practice/)。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】同时运用两种思维解题](https://labuladong.online/algo/problem-set/binary-tree-combine-two-view/) + - [【强化练习】用「遍历」思维解题 I](https://labuladong.online/algo/problem-set/binary-tree-traverse-i/) + - [【强化练习】用「遍历」思维解题 II](https://labuladong.online/algo/problem-set/binary-tree-traverse-ii/) + - [【强化练习】用「遍历」思维解题 III](https://labuladong.online/algo/problem-set/binary-tree-traverse-iii/) + - [二叉搜索树心法(构造篇)](https://labuladong.online/algo/data-structure/bst-part3/) + - [二叉搜索树心法(特性篇)](https://labuladong.online/algo/data-structure/bst-part1/) + - [二叉树心法(构造篇)](https://labuladong.online/algo/data-structure/binary-tree-part2/) + - [分治算法详解:运算优先级](https://labuladong.online/algo/fname.html?fname=分治算法) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer 26. 树的子结构](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 27. 二叉树的镜像](https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof/?show=1) | 🟢 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2272.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2272.md" new file mode 100644 index 0000000000..c116d63202 --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2272.md" @@ -0,0 +1,670 @@ +# 二叉树心法(构造篇) + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [105. Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) | [105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) | 🟠 | +| [106. Construct Binary Tree from Inorder and Postorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) | [106. 从中序与后序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) | 🟠 | +| [654. Maximum Binary Tree](https://leetcode.com/problems/maximum-binary-tree/) | [654. 最大二叉树](https://leetcode.cn/problems/maximum-binary-tree/) | 🟠 | +| [889. Construct Binary Tree from Preorder and Postorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-postorder-traversal/) | [889. 根据前序和后序遍历构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树结构基础](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) +> - [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) +> - [二叉树心法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + +本文是承接 [二叉树心法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 的第二篇文章,先复述一下前文总结的二叉树解题总纲: + +> [!NOTE] +> 二叉树解题的思维模式分两类: +> +> **1、是否可以通过遍历一遍二叉树得到答案**?如果可以,用一个 `traverse` 函数配合外部变量来实现,这叫「遍历」的思维模式。 +> +> **2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案**?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。 +> +> 无论使用哪种思维模式,你都需要思考: +> +> **如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做**?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。 + +第一篇文章 [二叉树心法(思维篇)](https://labuladong.online/algo/data-structure/binary-tree-part1/) 讲了「遍历」和「分解问题」两种思维方式,本文讲二叉树的构造类问题。 + +**二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树**。 + +接下来直接看题。 + +## 构造最大二叉树 + +先来道简单的,这是力扣第 654 题「最大二叉树」,题目如下: + + + + + + + +```java +// 函数签名如下 +TreeNode constructMaximumBinaryTree(int[] nums); +``` + +每个二叉树节点都可以认为是一棵子树的根节点,对于根节点,首先要做的当然是把想办法把自己先构造出来,然后想办法构造自己的左右子树。 + +所以,我们要遍历数组把找到最大值 `maxVal`,从而把根节点 `root` 做出来,然后对 `maxVal` 左边的数组和右边的数组进行递归构建,作为 `root` 的左右子树。 + +按照题目给出的例子,输入的数组为 `[3,2,1,6,0,5]`,对于整棵树的根节点来说,其实在做这件事: + +```java +TreeNode constructMaximumBinaryTree([3,2,1,6,0,5]) { + // 找到数组中的最大值 + TreeNode root = new TreeNode(6); + // 递归调用构造左右子树 + root.left = constructMaximumBinaryTree([3,2,1]); + root.right = constructMaximumBinaryTree([0,5]); + return root; +} + +// 当前 nums 中的最大值就是根节点,然后根据索引递归调用左右数组构造左右子树即可 +// 再详细一点,就是如下伪码 +TreeNode constructMaximumBinaryTree(int[] nums) { + if (nums is empty) return null; + // 找到数组中的最大值 + int maxVal = Integer.MIN_VALUE; + int index = 0; + for (int i = 0; i < nums.length; i++) { + if (nums[i] > maxVal) { + maxVal = nums[i]; + index = i; + } + } + + TreeNode root = new TreeNode(maxVal); + // 递归调用构造左右子树 + root.left = constructMaximumBinaryTree(nums[0..index-1]); + root.right = constructMaximumBinaryTree(nums[index+1..nums.length-1]); + return root; +} +``` + +**当前 `nums` 中的最大值就是根节点,然后根据索引递归调用左右数组构造左右子树即可**。 + +明确了思路,我们可以重新写一个辅助函数 `build`,来控制 `nums` 的索引: + +```java +class Solution { + + public TreeNode constructMaximumBinaryTree(int[] nums) { + return build(nums, 0, nums.length - 1); + } + + // 定义:将 nums[lo..hi] 构造成符合条件的树,返回根节点 + TreeNode build(int[] nums, int lo, int hi) { + // base case + if (lo > hi) { + return null; + } + + // 找到数组中的最大值和对应的索引 + int index = -1, maxVal = Integer.MIN_VALUE; + for (int i = lo; i <= hi; i++) { + if (maxVal < nums[i]) { + index = i; + maxVal = nums[i]; + } + } + + // 先构造出根节点 + TreeNode root = new TreeNode(maxVal); + // 递归调用构造左右子树 + root.left = build(nums, lo, index - 1); + root.right = build(nums, index + 1, hi); + + return root; + } +} +``` + + +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +至此,这道题就做完了,还是挺简单的对吧,下面看两道更困难一些的。 + +## 通过前序和中序遍历结果构造二叉树 + +力扣第 105 题「从前序和中序遍历序列构造二叉树」就是这道经典题目,面试笔试中常考: + + + + + + + +```java +// 函数签名如下 +TreeNode buildTree(int[] preorder, int[] inorder); +``` + +废话不多说,直接来想思路,首先思考,根节点应该做什么。 + +**类似上一题,我们肯定要想办法确定根节点的值,把根节点做出来,然后递归构造左右子树即可**。 + +我们先来回顾一下,前序遍历和中序遍历的结果有什么特点? + +```java +void traverse(TreeNode root) { + // 前序遍历 + preorder.add(root.val); + traverse(root.left); + traverse(root.right); +} + +void traverse(TreeNode root) { + traverse(root.left); + // 中序遍历 + inorder.add(root.val); + traverse(root.right); +} +``` + +前文 [二叉树就那几个框架](https://labuladong.online/algo/data-structure/flatten-nested-list-iterator/) 写过,这样的遍历顺序差异,导致了 `preorder` 和 `inorder` 数组中的元素分布有如下特点: + +![](https://labuladong.online/algo/images/binary-tree-ii/1.jpeg) + +找到根节点是很简单的,前序遍历的第一个值 `preorder[0]` 就是根节点的值。 + +关键在于如何通过根节点的值,将 `preorder` 和 `inorder` 数组划分成两半,构造根节点的左右子树? + +换句话说,对于以下代码中的 `?` 部分应该填入什么: + +```java +TreeNode buildTree(int[] preorder, int[] inorder) { + // 根据函数定义,用 preorder 和 inorder 构造二叉树 + return build(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); +} + +// build 函数的定义: +// 若前序遍历数组为 preorder[preStart..preEnd], +// 中序遍历数组为 inorder[inStart..inEnd], +// 构造二叉树,返回该二叉树的根节点 +TreeNode build(int[] preorder, int preStart, int preEnd, + int[] inorder, int inStart, int inEnd) { + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + int index = 0; + for (int i = inStart; i <= inEnd; i++) { + if (inorder[i] == rootVal) { + index = i; + break; + } + } + + TreeNode root = new TreeNode(rootVal); + // 递归构造左右子树 + root.left = build(preorder, ?, ?, + inorder, ?, ?); + + root.right = build(preorder, ?, ?, + inorder, ?, ?); + return root; +} +``` + +对于代码中的 `rootVal` 和 `index` 变量,就是下图这种情况: + +![](https://labuladong.online/algo/images/binary-tree-ii/2.jpeg) + +另外,也有读者注意到,通过 for 循环遍历的方式去确定 `index` 效率不算高,可以进一步优化。 + +因为题目说二叉树节点的值不存在重复,所以可以使用一个 HashMap 存储元素到索引的映射,这样就可以直接通过 HashMap 查到 `rootVal` 对应的 `index`: + +```java +// 存储 inorder 中值到索引的映射 +HashMap valToIndex = new HashMap<>(); + +public TreeNode buildTree(int[] preorder, int[] inorder) { + for (int i = 0; i < inorder.length; i++) { + valToIndex.put(inorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); +} + +TreeNode build(int[] preorder, int preStart, int preEnd, + int[] inorder, int inStart, int inEnd) { + int rootVal = preorder[preStart]; + // 避免 for 循环寻找 rootVal + int index = valToIndex.get(rootVal); + // ... +} +``` + +现在我们来看图做填空题,下面这几个问号处应该填什么: + +```java +root.left = build(preorder, ?, ?, + inorder, ?, ?); + +root.right = build(preorder, ?, ?, + inorder, ?, ?); +``` + +对于左右子树对应的 `inorder` 数组的起始索引和终止索引比较容易确定: + +![](https://labuladong.online/algo/images/binary-tree-ii/3.jpeg) + +```java +root.left = build(preorder, ?, ?, + inorder, inStart, index - 1); + +root.right = build(preorder, ?, ?, + inorder, index + 1, inEnd); +``` + +对于 `preorder` 数组呢?如何确定左右数组对应的起始索引和终止索引? + +这个可以通过左子树的节点数推导出来,假设左子树的节点数为 `leftSize`,那么 `preorder` 数组上的索引情况是这样的: + +![](https://labuladong.online/algo/images/binary-tree-ii/4.jpeg) + +看着这个图就可以把 `preorder` 对应的索引写进去了: + +```java +int leftSize = index - inStart; + +root.left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + +root.right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); +``` + +至此,整个算法思路就完成了,我们再补一补 base case 即可写出解法代码: + +```java +class Solution { + // 存储 inorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode buildTree(int[] preorder, int[] inorder) { + for (int i = 0; i < inorder.length; i++) { + valToIndex.put(inorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); + } + + // build 函数的定义: + // 若前序遍历数组为 preorder[preStart..preEnd], + // 中序遍历数组为 inorder[inStart..inEnd], + // 构造二叉树,返回该二叉树的根节点 + TreeNode build(int[] preorder, int preStart, int preEnd, + int[] inorder, int inStart, int inEnd) { + + if (preStart > preEnd) { + return null; + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex.get(rootVal); + + int leftSize = index - inStart; + + // 先构造出当前根节点 + TreeNode root = new TreeNode(rootVal); + // 递归构造左右子树 + root.left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + + root.right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); + return root; + } +} +``` + +我们的主函数只要调用 `buildTree` 函数即可,你看着函数这么多参数,解法这么多代码,似乎比我们上面讲的那道题难很多,让人望而生畏,实际上呢,这些参数无非就是控制数组起止位置的,画个图就能解决了。 + +## 通过后序和中序遍历结果构造二叉树 + +类似上一题,这次我们利用**后序**和**中序**遍历的结果数组来还原二叉树,这是力扣第 106 题「从后序和中序遍历序列构造二叉树」: + + + + + + + +```java +// 函数签名如下 +TreeNode buildTree(int[] inorder, int[] postorder); +``` + +类似的,看下后序和中序遍历的特点: + +```java +void traverse(TreeNode root) { + traverse(root.left); + traverse(root.right); + // 后序遍历 + postorder.add(root.val); +} + +void traverse(TreeNode root) { + traverse(root.left); + // 中序遍历 + inorder.add(root.val); + traverse(root.right); +} +``` + +这样的遍历顺序差异,导致了 `postorder` 和 `inorder` 数组中的元素分布有如下特点: + +![](https://labuladong.online/algo/images/binary-tree-ii/5.jpeg) + +这道题和上一题的关键区别是,后序遍历和前序遍历相反,根节点对应的值为 `postorder` 的最后一个元素。 + +整体的算法框架和上一题非常类似,我们依然写一个辅助函数 `build`: + +```java +class Solution { + // 存储 inorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode buildTree(int[] inorder, int[] postorder) { + for (int i = 0; i < inorder.length; i++) { + valToIndex.put(inorder[i], i); + } + return build(inorder, 0, inorder.length - 1, + postorder, 0, postorder.length - 1); + } + + // build 函数的定义: + // 后序遍历数组为 postorder[postStart..postEnd], + // 中序遍历数组为 inorder[inStart..inEnd], + // 构造二叉树,返回该二叉树的根节点 + TreeNode build(int[] inorder, int inStart, int inEnd, + int[] postorder, int postStart, int postEnd) { + // root 节点对应的值就是后序遍历数组的最后一个元素 + int rootVal = postorder[postEnd]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex.get(rootVal); + + TreeNode root = new TreeNode(rootVal); + // 递归构造左右子树 + root.left = build(inorder, ?, ?, + postorder, ?, ?); + + root.right = build(inorder, ?, ?, + postorder, ?, ?); + return root; + } +} +``` + +现在 `postoder` 和 `inorder` 对应的状态如下: + +![](https://labuladong.online/algo/images/binary-tree-ii/6.jpeg) + +我们可以按照上图将问号处的索引正确填入: + +```java +int leftSize = index - inStart; + +root.left = build(inorder, inStart, index - 1, + postorder, postStart, postStart + leftSize - 1); + +root.right = build(inorder, index + 1, inEnd, + postorder, postStart + leftSize, postEnd - 1); +``` + +综上,可以写出完整的解法代码: + +```java +class Solution { + // 存储 inorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode buildTree(int[] inorder, int[] postorder) { + for (int i = 0; i < inorder.length; i++) { + valToIndex.put(inorder[i], i); + } + return build(inorder, 0, inorder.length - 1, + postorder, 0, postorder.length - 1); + } + + // build 函数的定义: + // 后序遍历数组为 postorder[postStart..postEnd], + // 中序遍历数组为 inorder[inStart..inEnd], + // 构造二叉树,返回该二叉树的根节点 + TreeNode build(int[] inorder, int inStart, int inEnd, + int[] postorder, int postStart, int postEnd) { + + if (inStart > inEnd) { + return null; + } + // root 节点对应的值就是后序遍历数组的最后一个元素 + int rootVal = postorder[postEnd]; + // rootVal 在中序遍历数组中的索引 + int index = valToIndex.get(rootVal); + // 左子树的节点个数 + int leftSize = index - inStart; + TreeNode root = new TreeNode(rootVal); + // 递归构造左右子树 + root.left = build(inorder, inStart, index - 1, + postorder, postStart, postStart + leftSize - 1); + + root.right = build(inorder, index + 1, inEnd, + postorder, postStart + leftSize, postEnd - 1); + return root; + } +} +``` + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +有了前一题的铺垫,这道题很快就解决了,无非就是 `rootVal` 变成了最后一个元素,再改改递归函数的参数而已,只要明白二叉树的特性,也不难写出来。 + +## 通过后序和前序遍历结果构造二叉树 + +这是力扣第 889 题「根据前序和后序遍历构造二叉树」,给你输入二叉树的前序和后序遍历结果,让你还原二叉树的结构。 + +函数签名如下: + +```java +TreeNode constructFromPrePost(int[] preorder, int[] postorder); +``` + +这道题和前两道题有一个本质的区别: + +**通过前序中序,或者后序中序遍历结果可以确定唯一一棵原始二叉树,但是通过前序后序遍历结果无法确定唯一的原始二叉树**。 + +题目也说了,如果有多种可能的还原结果,你可以返回任意一种。 + +为什么呢?我们说过,构建二叉树的套路很简单,先找到根节点,然后找到并递归构造左右子树即可。 + +前两道题,可以通过前序或者后序遍历结果找到根节点,然后根据中序遍历结果确定左右子树(题目说了树中没有 `val` 相同的节点)。 + +这道题,你可以确定根节点,但是无法确切的知道左右子树有哪些节点。 + +举个例子,比如给你这个输入: + +``` +preorder = [1,2,3], postorder = [3,2,1] +``` + +下面这两棵树都是符合条件的,但显然它们的结构不同: + +![](https://labuladong.online/algo/images/binary-tree-ii/7.png) + +不过话说回来,用后序遍历和前序遍历结果还原二叉树,解法逻辑上和前两道题差别不大,也是通过控制左右子树的索引来构建: + +**1、首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值**。 + +**2、然后把前序遍历结果的第二个元素作为左子树的根节点的值**。 + +**3、在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可**。 + +![](https://labuladong.online/algo/images/binary-tree-ii/8.jpeg) + +详情见代码。 + +```java +class Solution { + // 存储 postorder 中值到索引的映射 + HashMap valToIndex = new HashMap<>(); + + public TreeNode constructFromPrePost(int[] preorder, int[] postorder) { + for (int i = 0; i < postorder.length; i++) { + valToIndex.put(postorder[i], i); + } + return build(preorder, 0, preorder.length - 1, + postorder, 0, postorder.length - 1); + } + + // 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd] + // 构建二叉树,并返回根节点。 + TreeNode build(int[] preorder, int preStart, int preEnd, + int[] postorder, int postStart, int postEnd) { + if (preStart > preEnd) { + return null; + } + if (preStart == preEnd) { + return new TreeNode(preorder[preStart]); + } + + // root 节点对应的值就是前序遍历数组的第一个元素 + int rootVal = preorder[preStart]; + // root.left 的值是前序遍历第二个元素 + // 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点 + // 确定 preorder 和 postorder 中左右子树的元素区间 + int leftRootVal = preorder[preStart + 1]; + // leftRootVal 在后序遍历数组中的索引 + int index = valToIndex.get(leftRootVal); + // 左子树的元素个数 + int leftSize = index - postStart + 1; + + // 先构造出当前根节点 + TreeNode root = new TreeNode(rootVal); + // 递归构造左右子树 + // 根据左子树的根节点索引和元素个数推导左右子树的索引边界 + root.left = build(preorder, preStart + 1, preStart + leftSize, + postorder, postStart, index); + root.right = build(preorder, preStart + leftSize + 1, preEnd, + postorder, index + 1, postEnd - 1); + + return root; + } +} +``` + + +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +代码和前两道题非常类似,我们可以看着代码思考一下,为什么通过前序遍历和后序遍历结果还原的二叉树可能不唯一呢? + +关键在这一句: + +```java +int leftRootVal = preorder[preStart + 1]; +``` + +我们假设前序遍历的第二个元素是左子树的根节点,但实际上左子树有可能是空指针,那么这个元素就应该是右子树的根节点。由于这里无法确切进行判断,所以导致了最终答案的不唯一。 + +至此,通过前序和后序遍历结果还原二叉树的问题也解决了。 + +最后呼应下前文,**二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树**。先找出根节点,然后根据根节点的值找到左右子树的元素,进而递归构建出左右子树。 + +现在你是否明白其中的玄妙了呢? + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】二叉搜索树经典例题 I](https://labuladong.online/algo/problem-set/bst1/) + - [【强化练习】用「分解问题」思维解题 I](https://labuladong.online/algo/problem-set/binary-tree-divide-i/) + - [【强化练习】用「分解问题」思维解题 II](https://labuladong.online/algo/problem-set/binary-tree-divide-ii/) + - [二叉搜索树心法(特性篇)](https://labuladong.online/algo/data-structure/bst-part1/) + - [二叉树心法(序列化篇)](https://labuladong.online/algo/data-structure/serialize-and-deserialize-binary-tree/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1008. Construct Binary Search Tree from Preorder Traversal](https://leetcode.com/problems/construct-binary-search-tree-from-preorder-traversal/?show=1) | [1008. 前序遍历构造二叉搜索树](https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/?show=1) | 🟠 | +| - | [剑指 Offer 07. 重建二叉树](https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 33. 二叉搜索树的后序遍历序列](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" index e91c439012..3f8b9b4362 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" @@ -1,126 +1,289 @@ -### 如何使用单调栈解题 +# 单调栈算法模板解决三道例题 -栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。 -单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。 -听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一种典型的问题,叫做 Next Greater Element。本文用讲解单调队列的算法模版解决这类问题,并且探讨处理「循环数组」的策略。 +![](https://labuladong.online/algo/images/souyisou1.png) -首先,讲解 Next Greater Number 的原始问题:给你一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。不好用语言解释清楚,直接上一个例子: +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。 -解释:第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。 -这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 O(n^2)。 +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: -这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的 Next Greater Number 呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的 Next Greater Number,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。 +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [496. Next Greater Element I](https://leetcode.com/problems/next-greater-element-i/) | [496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/) | 🟢 | +| [503. Next Greater Element II](https://leetcode.com/problems/next-greater-element-ii/) | [503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii/) | 🟠 | +| [739. Daily Temperatures](https://leetcode.com/problems/daily-temperatures/) | [739. 每日温度](https://leetcode.cn/problems/daily-temperatures/) | 🟠 | +| - | [剑指 Offer II 038. 每日温度](https://leetcode.cn/problems/iIQa4I/) | 🟠 | -![ink-image](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/1.png) +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) +> - [链表基础](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) +> - [队列/栈基础](https://labuladong.online/algo/data-structure-basic/queue-stack-basic/) + +栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。 + +听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一类典型的问题,比如「下一个更大元素」,「上一个更小元素」等。本文用讲解单调队列的算法模版解决「下一个更大元素」相关问题,并且探讨处理「循环数组」的策略。至于其他的变体和经典例题,我会在下一篇文章 [单调栈变体和经典习题](https://labuladong.online/algo/problem-set/monotonic-stack/) 讲解。 + +## 单调栈模板 + +现在给你出这么一道题:输入一个数组 `nums`,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。函数签名如下: + +```java +int[] calculateGreaterElement(int[] nums); +``` + +比如说,输入一个数组 `nums = [2,1,2,4,3]`,你返回数组 `[4,2,4,-1,-1]`。因为第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。 + +这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 $O(n^2)$。 + +这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的下一个更大元素呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下一个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。 + +![](https://labuladong.online/algo/images/monotonic-stack/1.jpeg) 这个情景很好理解吧?带着这个抽象的情景,先来看下代码。 -```cpp -vector nextGreaterElement(vector& nums) { - vector ans(nums.size()); // 存放答案的数组 - stack s; - for (int i = nums.size() - 1; i >= 0; i--) { // 倒着往栈里放 - while (!s.empty() && s.top() <= nums[i]) { // 判定个子高矮 - s.pop(); // 矮个起开,反正也被挡着了。。。 +```java +int[] calculateGreaterElement(int[] nums) { + int n = nums.length; + // 存放答案的数组 + int[] res = new int[n]; + Stack s = new Stack<>(); + // 倒着往栈里放 + for (int i = n - 1; i >= 0; i--) { + // 判定个子高矮 + while (!s.isEmpty() && s.peek() <= nums[i]) { + // 矮个起开,反正也被挡着了。。。 + s.pop(); } - ans[i] = s.empty() ? -1 : s.top(); // 这个元素身后的第一个高个 - s.push(nums[i]); // 进队,接受之后的身高判定吧! + // nums[i] 身后的更大元素 + res[i] = s.isEmpty() ? -1 : s.peek(); + s.push(nums[i]); } - return ans; + return res; } ``` -这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个“高个”元素之间的元素排除,因为他们的存在没有意义,前面挡着个“更高”的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。 +这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个「个子高」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的下一个更大元素了。 -这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 O(n^2),但是实际上这个算法的复杂度只有 O(n)。 +这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 $O(n^2)$,但是实际上这个算法的复杂度只有 $O(n)$。 -分析它的时间复杂度,要从整体来看:总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop 一次,没有任何冗余操作。所以总的计算规模是和元素规模 n 成正比的,也就是 O(n) 的复杂度。 +分析它的时间复杂度,要从整体来看:总共有 `n` 个元素,每个元素都被 `push` 入栈了一次,而最多会被 `pop` 一次,没有任何冗余操作。所以总的计算规模是和元素规模 `n` 成正比的,也就是 $O(n)$ 的复杂度。 -现在,你已经掌握了单调栈的使用技巧,来一个简单的变形来加深一下理解。 +## 问题变形 -给你一个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近几天的天气气温(这气温是铁板烧?不是的,这里用的华氏度)。你返回一个数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0 。 +单调栈的代码实现比较简单,下面来看一些具体题目。 -举例:给你 T = [73, 74, 75, 71, 69, 72, 76, 73],你返回 [1, 1, 4, 2, 1, 1, 0, 0]。 +### 496. 下一个更大元素 I -解释:第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温。后面的同理。 +首先来一个简单的变形,力扣第 496 题「下一个更大元素 I」: -你已经对 Next Greater Number 类型问题有些敏感了,这个问题本质上也是找 Next Greater Number,只不过现在不是问你 Next Greater Number 是多少,而是问你当前距离 Next Greater Number 的距离而已。 + -相同类型的问题,相同的思路,直接调用单调栈的算法模板,稍作改动就可以啦,直接上代码把。 +这道题给你输入两个数组 `nums1` 和 `nums2`,让你求 `nums1` 中的元素在 `nums2` 中的下一个更大元素,函数签名如下: -```cpp -vector dailyTemperatures(vector& T) { - vector ans(T.size()); - stack s; // 这里放元素索引,而不是元素 - for (int i = T.size() - 1; i >= 0; i--) { - while (!s.empty() && T[s.top()] <= T[i]) { - s.pop(); +```java +int[] nextGreaterElement(int[] nums1, int[] nums2) +``` + +其实和把我们刚才的代码改一改就可以解决这道题了,因为题目说 `nums1` 是 `nums2` 的子集,那么我们先把 `nums2` 中每个元素的下一个更大元素算出来存到一个映射里,然后再让 `nums1` 中的元素去查表即可: + +```java +class Solution { + public int[] nextGreaterElement(int[] nums1, int[] nums2) { + // 记录 nums2 中每个元素的下一个更大元素 + int[] greater = calculateGreaterElement(nums2); + // 转化成映射:元素 x -> x 的下一个最大元素 + HashMap greaterMap = new HashMap<>(); + for (int i = 0; i < nums2.length; i++) { + greaterMap.put(nums2[i], greater[i]); } - ans[i] = s.empty() ? 0 : (s.top() - i); // 得到索引间距 - s.push(i); // 加入索引,而不是元素 + // nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果 + int[] res = new int[nums1.length]; + for (int i = 0; i < nums1.length; i++) { + res[i] = greaterMap.get(nums1[i]); + } + return res; + } + + int[] calculateGreaterElement(int[] nums) { + // 见上文 } - return ans; } ``` +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ -单调栈讲解完毕。下面开始另一个重点:如何处理「循环数组」。 -同样是 Next Greater Number,现在假设给你的数组是个环形的,如何处理? -给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,4]。拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4 。 +### 739. 每日温度 -![ink-image](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/2.png) +再看看力扣第 739 题「每日温度」: +给你一个数组 `temperatures`,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。函数签名如下: -首先,计算机的内存都是线性的,没有真正意义上的环形数组,但是我们可以模拟出环形数组的效果,一般是通过 % 运算符求模(余数),获得环形特效: +```java +int[] dailyTemperatures(int[] temperatures); +``` + +比如说给你输入 `temperatures = [73,74,75,71,69,76]`,你返回 `[1,1,3,2,1,0]`。因为第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温,后面的同理。 + +这个问题本质上也是找下一个更大元素,只不过现在不是问你下一个更大元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离而已。 + +相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧: + +```java +class Solution { + public int[] dailyTemperatures(int[] temperatures) { + int n = temperatures.length; + int[] res = new int[n]; + // 这里放元素索引,而不是元素 + Stack s = new Stack<>(); + // 单调栈模板 + for (int i = n - 1; i >= 0; i--) { + while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) { + s.pop(); + } + // 得到索引间距 + res[i] = s.isEmpty() ? 0 : (s.peek() - i); + // 将索引入栈,而不是元素 + s.push(i); + } + return res; + } +} +``` + +单调栈讲解完毕,下面开始另一个重点:如何处理「循环数组」。 + +## 如何处理环形数组 + +同样是求下一个更大元素,现在假设给你的数组是个环形的,如何处理?力扣第 503 题「下一个更大元素 II」就是这个问题:输入一个「环形数组」,请你计算其中每个元素的下一个更大元素。 + +比如输入 `[2,1,2,4,3]`,你应该返回 `[4,2,4,-1,4]`,因为拥有了环形属性,**最后一个元素 3 绕了一圈后找到了比自己大的元素 4**。 + +如果你看过基础知识章节的 [环形数组技巧](https://labuladong.online/algo/data-structure-basic/cycle-array/) 应该比较熟悉,我们一般是通过 % 运算符求模(余数),来模拟环形特效: ```java int[] arr = {1,2,3,4,5}; int n = arr.length, index = 0; while (true) { + // 在环形数组中转圈 print(arr[index % n]); index++; } ``` -回到 Next Greater Number 的问题,增加了环形属性后,问题的难点在于:这个 Next 的意义不仅仅是当前元素的右边了,有可能出现在当前元素的左边(如上例)。 +这个问题肯定还是要用单调栈的解题模板,但难点在于,比如输入是 `[2,1,2,4,3]`,对于最后一个元素 3,如何找到元素 4 作为下一个更大元素。 -明确问题,问题就已经解决了一半了。我们可以考虑这样的思路:将原始数组“翻倍”,就是在后面再接一个原始数组,这样的话,按照之前“比身高”的流程,每个元素不仅可以比较自己右边的元素,而且也可以和左边的元素比较了。 +**对于这种需求,常用套路就是将数组长度翻倍**: -![ink-image (2)](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/3.png) +![](https://labuladong.online/algo/images/monotonic-stack/2.jpeg) -怎么实现呢?你当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,我们可以不用构造新数组,而是利用循环数组的技巧来模拟。直接看代码吧: +这样,元素 3 就可以找到元素 4 作为下一个更大元素了,而且其他的元素都可以被正确地计算。 -```cpp -vector nextGreaterElements(vector& nums) { - int n = nums.size(); - vector res(n); // 存放结果 - stack s; - // 假装这个数组长度翻倍了 - for (int i = 2 * n - 1; i >= 0; i--) { - while (!s.empty() && s.top() <= nums[i % n]) - s.pop(); - res[i % n] = s.empty() ? -1 : s.top(); - s.push(nums[i % n]); +有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**。直接看代码吧: + +```java +class Solution { + public int[] nextGreaterElements(int[] nums) { + int n = nums.length; + int[] res = new int[n]; + Stack s = new Stack<>(); + // 数组长度加倍模拟环形数组 + for (int i = 2 * n - 1; i >= 0; i--) { + // 索引 i 要求模,其他的和模板一样 + while (!s.isEmpty() && s.peek() <= nums[i % n]) { + s.pop(); + } + res[i % n] = s.isEmpty() ? -1 : s.peek(); + s.push(nums[i % n]); + } + return res; } - return res; } ``` -至此,你已经掌握了单调栈的设计方法及代码模板,学会了解决 Next Greater Number,并能够处理循环数组了。 -你的在看,是对我的鼓励。关注公众号:labuladong +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +这样,就可以巧妙解决环形数组的问题,时间复杂度 $O(N)$。 + +最后提出一些问题吧,本文提供的单调栈模板是 `nextGreaterElement` 函数,可以计算每个元素的下一个更大元素,但如果题目让你计算上一个更大元素,或者计算上一个更大或相等的元素,应该如何修改对应的模板呢?而且在实际应用中,题目不会直接让你计算下一个(上一个)更大(小)的元素,你如何把问题转化成单调栈相关的问题呢? + +我会在 [单调栈的几种变体及习题](https://labuladong.online/algo/problem-set/monotonic-stack/) 对比单调栈的几种其他形式,并在 给出单调栈的经典例题。更多数据结构设计类题目参见 [数据结构设计经典习题](https://labuladong.online/algo/problem-set/ds-design/)。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】单调栈的几种变体及经典习题](https://labuladong.online/algo/problem-set/monotonic-stack/) + - [【强化练习】单调队列的通用实现及经典习题](https://labuladong.online/algo/problem-set/monotonic-queue/) + - [一个方法团灭 LeetCode 打家劫舍问题](https://labuladong.online/algo/dynamic-programming/house-robber/) + - [单调队列结构解决滑动窗口问题](https://labuladong.online/algo/data-structure/monotonic-queue/) + - [常用的位操作](https://labuladong.online/algo/frequency-interview/bitwise-operation/) + - [拓展:数组去重问题(困难版)](https://labuladong.online/algo/frequency-interview/remove-duplicate-letters/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1019. Next Greater Node In Linked List](https://leetcode.com/problems/next-greater-node-in-linked-list/?show=1) | [1019. 链表中的下一个更大节点](https://leetcode.cn/problems/next-greater-node-in-linked-list/?show=1) | 🟠 | +| [1944. Number of Visible People in a Queue](https://leetcode.com/problems/number-of-visible-people-in-a-queue/?show=1) | [1944. 队列中可以看到的人数](https://leetcode.cn/problems/number-of-visible-people-in-a-queue/?show=1) | 🔴 | +| [402. Remove K Digits](https://leetcode.com/problems/remove-k-digits/?show=1) | [402. 移掉 K 位数字](https://leetcode.cn/problems/remove-k-digits/?show=1) | 🟠 | +| [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/?show=1) | [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/?show=1) | 🔴 | +| [901. Online Stock Span](https://leetcode.com/problems/online-stock-span/?show=1) | [901. 股票价格跨度](https://leetcode.cn/problems/online-stock-span/?show=1) | 🟠 | +| [918. Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray/?show=1) | [918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/?show=1) | 🟠 | + +
+
+ + +**_____________** -[上一篇:二叉搜索树操作集锦](../数据结构系列/二叉搜索树操作集锦.md) -[下一篇:特殊数据结构:单调队列](../数据结构系列/单调队列.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" index 5c466f07cc..45514d03d4 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" @@ -1,35 +1,95 @@ -# 特殊数据结构:单调队列 +# 单调队列结构解决滑动窗口问题 -前文讲了一种特殊的数据结构「单调栈」monotonic stack,解决了一类问题「Next Greater Number」,本文写一个类似的数据结构「单调队列」。 -也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素单调递增(或递减)。这个数据结构有什么用?可以解决滑动窗口的一系列问题。 -看一道 LeetCode 题目,难度 hard: +![](https://labuladong.online/algo/images/souyisou1.png) -![](../pictures/单调队列/title.png) +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -### 一、搭建解题框架 -这道题不复杂,难点在于如何在 O(1) 时间算出每个「窗口」中的最大值,使得整个算法在线性时间完成。在之前我们探讨过类似的场景,得到一个结论: -在一堆数字中,已知最值,如果给这堆数添加一个数,那么比较一下就可以很快算出最值;但如果减少一个数,就不一定能很快得到最值了,而要遍历所有数重新找最值。 +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [239. Sliding Window Maximum](https://leetcode.com/problems/sliding-window-maximum/) | [239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | 🔴 | +| [面试题59 - II. 队列的最大值 LCOF](https://leetcode.com/problems/dui-lie-de-zui-da-zhi-lcof/) | [面试题59 - II. 队列的最大值](https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) +> - [链表基础](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) +> - [队列/栈基础](https://labuladong.online/algo/data-structure-basic/queue-stack-basic/) + +前文用 [单调栈解决三道算法问题](https://labuladong.online/algo/data-structure/monotonic-stack/) 介绍了单调栈这种特殊数据结构,本文写一个类似的数据结构「单调队列」。 + +也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素全都是单调递增(或递减)的。 + +为啥要发明「单调队列」这种结构呢,主要是为了解决下面这个场景: + +**给你一个数组 `window`,已知其最值为 `A`,如果给 `window` 中添加一个数 `B`,那么比较一下 `A` 和 `B` 就可以立即算出新的最值;但如果要从 `window` 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 `A`,就需要遍历 `window` 中的所有元素重新寻找新的最值**。 + +这个场景很常见,但不用单调队列似乎也可以,比如 [优先级队列(二叉堆)](https://labuladong.online/algo/data-structure-basic/binary-heap-basic/) 就是专门用来动态寻找最值的,我创建一个大(小)顶堆,不就可以很快拿到最大(小)值了吗? + +如果单纯地维护最值的话,优先级队列很专业,队头元素就是最值。但优先级队列无法满足标准队列结构「先进先出」的**时间顺序**,因为优先级队列底层利用二叉堆对元素进行动态排序,元素的出队顺序是元素的大小顺序,和入队的先后顺序完全没有关系。 + +所以,现在需要一种新的队列结构,既能够维护队列元素「先进先出」的时间顺序,又能够正确维护队列中所有元素的最值,这就是「单调队列」结构。 + +「单调队列」这个数据结构主要用来辅助解决滑动窗口相关的问题,前文 [滑动窗口核心框架](https://labuladong.online/algo/essential-technique/sliding-window-framework/) 把滑动窗口算法作为双指针技巧的一部分进行了讲解,但有些稍微复杂的滑动窗口问题不能只靠两个指针来解决,需要上更先进的数据结构。 + +比方说,你注意看前文 [滑动窗口核心框架](https://labuladong.online/algo/essential-technique/sliding-window-framework/) 讲的几道题目,每当窗口扩大(`right++`)和窗口缩小(`left++`)时,你单凭移出和移入窗口的元素即可决定是否更新答案。 + +但本文开头说的那个判断一个窗口中最值的例子,你无法单凭移出窗口的那个元素更新窗口的最值,除非重新遍历所有元素,但这样的话时间复杂度就上来了,这是我们不希望看到的。 + +我们来看看力扣第 239 题「滑动窗口最大值」,就是一道标准的滑动窗口问题: + +给你输入一个数组 `nums` 和一个正整数 `k`,有一个大小为 `k` 的窗口在 `nums` 上从左至右滑动,请你输出每次窗口中 `k` 个元素的最大值。 + +函数签名如下: + +```java +int[] maxSlidingWindow(int[] nums, int k); +``` + +比如说力扣给出的一个示例: + +``` +输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 +输出:[3,3,5,5,6,7] +解释: +滑动窗口的位置 最大值 +--------------- ----- +[1 3 -1] -3 5 3 6 7 3 + 1 [3 -1 -3] 5 3 6 7 3 + 1 3 [-1 -3 5] 3 6 7 5 + 1 3 -1 [-3 5 3] 6 7 5 + 1 3 -1 -3 [5 3 6] 7 6 + 1 3 -1 -3 5 [3 6 7] 7 +``` + -回到这道题的场景,每个窗口前进的时候,要添加一个数同时减少一个数,所以想在 O(1) 的时间得出新的最值,就需要「单调队列」这种特殊的数据结构来辅助了。 -一个普通的队列一定有这两个操作: +接下来,我们就借助单调队列结构,用 $O(1)$ 时间算出每个滑动窗口中的最大值,使得整个算法在线性时间完成。 + +### 一、搭建解题框架 + +在介绍「单调队列」这种数据结构的 API 之前,先来对比一下 [普通的队列](https://labuladong.online/algo/data-structure-basic/queue-stack-basic/) 的标准 API 和单调队列实现的 API: ```java +// 普通队列的 API class Queue { + // enqueue 操作,在队尾加入元素 n void push(int n); - // 或 enqueue,在队尾加入元素 n + // dequeue 操作,删除队头元素 void pop(); - // 或 dequeue,删除队头元素 } -``` - -一个「单调队列」的操作也差不多: -```java +// 单调队列的 API class MonotonicQueue { // 在队尾添加元素 n void push(int n); @@ -40,153 +100,238 @@ class MonotonicQueue { } ``` -当然,这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来: +当然,单调队列这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来: -```cpp -vector maxSlidingWindow(vector& nums, int k) { - MonotonicQueue window; - vector res; - for (int i = 0; i < nums.size(); i++) { - if (i < k - 1) { //先把窗口的前 k - 1 填满 +```java +int[] maxSlidingWindow(int[] nums, int k) { + MonotonicQueue window = new MonotonicQueue(); + List res = new ArrayList<>(); + + for (int i = 0; i < nums.length; i++) { + if (i < k - 1) { + // 先把窗口的前 k - 1 填满 window.push(nums[i]); - } else { // 窗口开始向前滑动 + } else { + // 窗口开始向前滑动 + // 移入新元素 window.push(nums[i]); - res.push_back(window.max()); + // 将当前窗口中的最大元素记入结果 + res.add(window.max()); + // 移出最后的元素 window.pop(nums[i - k + 1]); - // nums[i - k + 1] 就是窗口最后的元素 } } - return res; + // 将 List 类型转化成 int[] 数组作为返回值 + int[] arr = new int[res.size()]; + for (int i = 0; i < res.size(); i++) { + arr[i] = res.get(i); + } + return arr; } ``` -![图示](../pictures/单调队列/1.png) +![](https://labuladong.online/algo/images/monotonic-queue/1.png) -这个思路很简单,能理解吧?下面我们开始重头戏,单调队列的实现。 -### 二、实现单调队列数据结构 -首先我们要认识另一种数据结构:deque,即双端队列。很简单: +这个思路很简单吧,下面我们开始重头戏,单调队列的实现。 -```java -class deque { - // 在队头插入元素 n - void push_front(int n); - // 在队尾插入元素 n - void push_back(int n); - // 在队头删除元素 - void pop_front(); - // 在队尾删除元素 - void pop_back(); - // 返回队头元素 - int front(); - // 返回队尾元素 - int back(); -} -``` +### 二、实现单调队列数据结构 -而且,这些操作的复杂度都是 O(1)。这其实不是啥稀奇的数据结构,用链表作为底层结构的话,很容易实现这些功能。 +观察滑动窗口的过程就能发现,实现「单调队列」必须使用一种数据结构支持在头部和尾部进行插入和删除,很明显 [双链表](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) 是满足这个条件的。 -「单调队列」的核心思路和「单调栈」类似。单调队列的 push 方法依然在队尾添加元素,但是要把前面比新元素小的元素都删掉: +「单调队列」的核心思路和「单调栈」类似,`push` 方法依然在队尾添加元素,但是要把前面比自己小的元素都删掉: -```cpp +```java class MonotonicQueue { -private: - deque data; -public: - void push(int n) { - while (!data.empty() && data.back() < n) - data.pop_back(); - data.push_back(n); + // 双链表,支持快速在头部和尾部增删元素 + // 维护其中的元素自尾部到头部单调递增 + private LinkedList maxq = new LinkedList<>(); + + // 在尾部添加一个元素 n,维护 maxq 的单调性质 + public void push(int n) { + // 将前面小于自己的元素都删除 + while (!maxq.isEmpty() && maxq.getLast() < n) { + maxq.pollLast(); + } + maxq.addLast(n); } -}; +} ``` -你可以想象,加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。 +你可以想象,加入数字的大小代表人的体重,体重大的会把前面体重不足的压扁,直到遇到更大的量级才停住。 -![](../pictures/单调队列/2.png) +![](https://labuladong.online/algo/images/monotonic-queue/3.png) -如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,因此我们的 max() API 可以可以这样写: +如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个**单调递减**的顺序,因此我们的 `max` 方法就很好写了,只要把队头元素返回即可;`pop` 方法也是操作队头,如果队头元素是待删除元素 `n`,那么就删除它: -```cpp -int max() { - return data.front(); -} -``` +```java +class MonotonicQueue { + // 为了节约篇幅,省略上文给出的代码部分... -pop() API 在队头删除元素 n,也很好写: + public int max() { + // 队头的元素肯定是最大的 + return maxq.getFirst(); + } -```cpp -void pop(int n) { - if (!data.empty() && data.front() == n) - data.pop_front(); + public void pop(int n) { + if (n == maxq.getFirst()) { + maxq.pollFirst(); + } + } } ``` -之所以要判断 `data.front() == n`,是因为我们想删除的队头元素 n 可能已经被「压扁」了,这时候就不用删除了: +`pop` 方法之所以要判断 `n == maxq.getFirst()`,是因为我们想删除的队头元素 `n` 可能已经在 `push` 的过程中被「压扁」了,可能已经不存在了,这种情况就不用删除了: -![](../pictures/单调队列/3.png) +![](https://labuladong.online/algo/images/monotonic-queue/2.png) 至此,单调队列设计完毕,看下完整的解题代码: -```cpp +```java +// 单调队列的实现 class MonotonicQueue { -private: - deque data; -public: - void push(int n) { - while (!data.empty() && data.back() < n) - data.pop_back(); - data.push_back(n); + LinkedList maxq = new LinkedList<>(); + public void push(int n) { + // 将小于 n 的元素全部删除 + while (!maxq.isEmpty() && maxq.getLast() < n) { + maxq.pollLast(); + } + // 然后将 n 加入尾部 + maxq.addLast(n); } - int max() { return data.front(); } + public int max() { + return maxq.getFirst(); + } - void pop(int n) { - if (!data.empty() && data.front() == n) - data.pop_front(); + public void pop(int n) { + if (n == maxq.getFirst()) { + maxq.pollFirst(); + } } -}; +} -vector maxSlidingWindow(vector& nums, int k) { - MonotonicQueue window; - vector res; - for (int i = 0; i < nums.size(); i++) { - if (i < k - 1) { //先填满窗口的前 k - 1 - window.push(nums[i]); - } else { // 窗口向前滑动 - window.push(nums[i]); - res.push_back(window.max()); - window.pop(nums[i - k + 1]); +class Solution { + int[] maxSlidingWindow(int[] nums, int k) { + MonotonicQueue window = new MonotonicQueue(); + List res = new ArrayList<>(); + + for (int i = 0; i < nums.length; i++) { + if (i < k - 1) { + // 先填满窗口的前 k - 1 + window.push(nums[i]); + } else { + // 窗口向前滑动,加入新数字 + window.push(nums[i]); + // 记录当前窗口的最大值 + res.add(window.max()); + // 移出旧数字 + window.pop(nums[i - k + 1]); + } + } + // 需要转成 int[] 数组再返回 + int[] arr = new int[res.size()]; + for (int i = 0; i < res.size(); i++) { + arr[i] = res.get(i); } + return arr; } - return res; } ``` -**三、算法复杂度分析** +有一点细节问题不要忽略,在实现 `MonotonicQueue` 时,我们使用了 Java 的 `LinkedList`,因为链表结构支持在头部和尾部快速增删元素;而在解法代码中的 `res` 则使用的 `ArrayList` 结构,因为后续会按照索引取元素,所以数组结构更合适。其他语言的实现也要注意这些细节。 + +关于单调队列 API 的时间复杂度,读者可能有疑惑:`push` 操作中含有 while 循环,最坏情况下的时间复杂度应该 $O(N)$ 呀,再加上一层 for 循环,本算法的时间复杂度应该是 $O(N^2)$ 才对吧? + +这里就用到了 [算法时空复杂度分析指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) 中讲到的摊还分析: + +单独看 `push` 操作,最坏时间复杂度确实是 $O(N)$,但是平均时间复杂度是 $O(1)$。我们一般用平均复杂度而不是最坏时间复杂度来衡量 API 接口,所以这个算法整体的时间复杂度是 $O(N)$,而不是 $O(N^2)$。 + +也可以这样从整体上分析:整个算法做的事情就是把 `nums` 中的每个元素加入和移出 `window` **至多一次**,不可能把同一个元素多次移入移出 `window`,所以整体的时间复杂度是 $O(N)$。 + +空间复杂度很容易分析,就是窗口的大小 $O(k)$。 + + + + + + + +### 拓展延伸 + +最后,我提出几个问题请大家思考: + +1、本文给出的 `MonotonicQueue` 类只实现了 `max` 方法,你是否能够再额外添加一个 `min` 方法,在 $O(1)$ 的时间返回队列中所有元素的最小值? + +2、本文给出的 `MonotonicQueue` 类的 `pop` 方法还需要接收一个参数,这不那么优雅,而且有悖于标准队列的 API,请你修复这个缺陷。 + +3、请你实现 `MonotonicQueue` 类的 `size` 方法,返回单调队列中元素的个数(注意,由于每次 `push` 方法都可能从底层的 `q` 列表中删除元素,所以 `q` 中的元素个数并不是单调队列的元素个数)。 + +也就是说,你是否能够实现单调队列的通用实现: + +```java +// 单调队列的通用实现,可以高效维护最大值和最小值 +class MonotonicQueue> { + + // 标准队列 API,向队尾加入元素 + public void push(E elem); + + // 标准队列 API,从队头弹出元素,符合先进先出的顺序 + public E pop(); + + // 标准队列 API,返回队列中的元素个数 + public int size(); + + // 单调队列特有 API,O(1) 时间计算队列中元素的最大值 + public E max(); + + // 单调队列特有 API,O(1) 时间计算队列中元素的最小值 + public E min(); +} +``` + +我将在 [单调队列通用实现及应用](https://labuladong.online/algo/problem-set/monotonic-queue/) 中给出单调队列的通用实现和经典习题。更多数据结构设计类题目参见 [数据结构设计经典习题](https://labuladong.online/algo/problem-set/ds-design/)。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】单调队列的通用实现及经典习题](https://labuladong.online/algo/problem-set/monotonic-queue/) + - [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + +

-读者可能疑惑,push 操作中含有 while 循环,时间复杂度不是 O(1) 呀,那么本算法的时间复杂度应该不是线性时间吧? -单独看 push 操作的复杂度确实不是 O(1),但是算法整体的复杂度依然是 O(N) 线性时间。要这样想,nums 中的每个元素最多被 push_back 和 pop_back 一次,没有任何多余操作,所以整体的复杂度还是 O(N)。 -空间复杂度就很简单了,就是窗口的大小 O(k)。 -**四、最后总结** +
+
+引用本文的题目 -有的读者可能觉得「单调队列」和「优先级队列」比较像,实际上差别很大的。 +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: -单调队列在添加元素的时候靠删除元素保持队列的单调性,相当于抽取出某个函数中单调递增(或递减)的部分;而优先级队列(二叉堆)相当于自动排序,差别大了去了。 +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1425. Constrained Subsequence Sum](https://leetcode.com/problems/constrained-subsequence-sum/?show=1) | [1425. 带限制的子序列和](https://leetcode.cn/problems/constrained-subsequence-sum/?show=1) | 🔴 | +| [1696. Jump Game VI](https://leetcode.com/problems/jump-game-vi/?show=1) | [1696. 跳跃游戏 VI](https://leetcode.cn/problems/jump-game-vi/?show=1) | 🟠 | +| [862. Shortest Subarray with Sum at Least K](https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/?show=1) | [862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/?show=1) | 🔴 | +| [918. Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray/?show=1) | [918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/?show=1) | 🟠 | +| - | [剑指 Offer 59 - I. 滑动窗口的最大值](https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/?show=1) | 🔴 | -赶紧去拿下 LeetCode 第 239 道题吧~ +
+
-坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:特殊数据结构:单调栈](../数据结构系列/单调栈.md) -[下一篇:设计Twitter](../数据结构系列/设计Twitter.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" new file mode 100644 index 0000000000..dfcadc1e2f --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" @@ -0,0 +1,394 @@ +# 图论算法基础 + +

+GitHub + + + +

+ +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [797. All Paths From Source to Target](https://leetcode.com/problems/all-paths-from-source-to-target/) | [797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/) | 🟠 +| - | [剑指 Offer II 110. 所有路径](https://leetcode.cn/problems/bP4bmD/) | 🟠 + +**-----------** + +> tip:本文有视频版:[图论基础及遍历算法](https://www.bilibili.com/video/BV19G41187cL/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +经常有读者问我「图」这种数据结构,其实我在 [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/abstraction-of-algorithm/) 中说过,虽然图可以玩出更多的算法,解决更复杂的问题,但本质上图可以认为是多叉树的延伸。 + +面试笔试很少出现图相关的问题,就算有,大多也是简单的遍历问题,基本上可以完全照搬多叉树的遍历。 + +那么,本文依然秉持我们号的风格,只讲「图」最实用的,离我们最近的部分,让你心里对图有个直观的认识,文末我给出了其他经典图论算法,理解本文后应该都可以拿下的。 + +### 图的逻辑结构和具体实现 + +一幅图是由**节点**和**边**构成的,逻辑结构如下: + +![](https://labuladong.online/algo/images/图/0.jpg) + +**什么叫「逻辑结构」?就是说为了方便研究,我们把图抽象成这个样子**。 + +根据这个逻辑结构,我们可以认为每个节点的实现如下: + + +```java +/* 图节点的逻辑结构 */ +class Vertex { + int id; + Vertex[] neighbors; +} +``` + +看到这个实现,你有没有很熟悉?它和我们之前说的多叉树节点几乎完全一样: + + +```java +/* 基本的 N 叉树节点 */ +class TreeNode { + int val; + TreeNode[] children; +} +``` + +所以说,图真的没啥高深的,本质上就是个高级点的多叉树而已,适用于树的 DFS/BFS 遍历算法,全部适用于图。 + +不过呢,上面的这种实现是「逻辑上的」,实际上我们很少用这个 `Vertex` 类实现图,而是用常说的**邻接表和邻接矩阵**来实现。 + +比如还是刚才那幅图: + +![](https://labuladong.online/algo/images/图/0.jpg) + +用邻接表和邻接矩阵的存储方式如下: + +![](https://labuladong.online/algo/images/图/2.jpeg) + +邻接表很直观,我把每个节点 `x` 的邻居都存到一个列表里,然后把 `x` 和这个列表关联起来,这样就可以通过一个节点 `x` 找到它的所有相邻节点。 + +邻接矩阵则是一个二维布尔数组,我们权且称为 `matrix`,如果节点 `x` 和 `y` 是相连的,那么就把 `matrix[x][y]` 设为 `true`(上图中绿色的方格代表 `true`)。如果想找节点 `x` 的邻居,去扫一圈 `matrix[x][..]` 就行了。 + +如果用代码的形式来表现,邻接表和邻接矩阵大概长这样: + + +```java +// 邻接表 +// graph[x] 存储 x 的所有邻居节点 +List[] graph; + +// 邻接矩阵 +// matrix[x][y] 记录 x 是否有一条指向 y 的边 +boolean[][] matrix; +``` + +**那么,为什么有这两种存储图的方式呢?肯定是因为他们各有优劣**。 + +对于邻接表,好处是占用的空间少。 + +你看邻接矩阵里面空着那么多位置,肯定需要更多的存储空间。 + +但是,邻接表无法快速判断两个节点是否相邻。 + +比如说我想判断节点 `1` 是否和节点 `3` 相邻,我要去邻接表里 `1` 对应的邻居列表里查找 `3` 是否存在。但对于邻接矩阵就简单了,只要看看 `matrix[1][3]` 就知道了,效率高。 + +所以说,使用哪一种方式实现图,要看具体情况。 + +::: tip + +在常规的算法题中,邻接表的使用会更频繁一些,主要是因为操作起来较为简单,但这不意味着邻接矩阵应该被轻视。矩阵是一个强有力的数学工具,图的一些隐晦性质可以借助精妙的矩阵运算展现出来。不过本文不准备引入数学内容,所以有兴趣的读者可以自行搜索学习。 + +::: + +最后,我们再明确一个图论中特有的**度**(degree)的概念,在无向图中,「度」就是每个节点相连的边的条数。 + +由于有向图的边有方向,所以有向图中每个节点「度」被细分为**入度**(indegree)和**出度**(outdegree),比如下图: + +![](https://labuladong.online/algo/images/图/0.jpg) + +其中节点 `3` 的入度为 3(有三条边指向它),出度为 1(它有 1 条边指向别的节点)。 + +好了,对于「图」这种数据结构,能看懂上面这些就绰绰够用了。 + +那你可能会问,我们上面说的这个图的模型仅仅是「有向无权图」,不是还有什么加权图,无向图,等等…… + +**其实,这些更复杂的模型都是基于这个最简单的图衍生出来的**。 + +**有向加权图怎么实现**?很简单呀: + +如果是邻接表,我们不仅仅存储某个节点 `x` 的所有邻居节点,还存储 `x` 到每个邻居的权重,不就实现加权有向图了吗? + +如果是邻接矩阵,`matrix[x][y]` 不再是布尔值,而是一个 int 值,0 表示没有连接,其他值表示权重,不就变成加权有向图了吗? + +如果用代码的形式来表现,大概长这样: + + +```java +// 邻接表 +// graph[x] 存储 x 的所有邻居节点以及对应的权重 +List[] graph; + +// 邻接矩阵 +// matrix[x][y] 记录 x 指向 y 的边的权重,0 表示不相邻 +int[][] matrix; +``` + +**无向图怎么实现**?也很简单,所谓的「无向」,是不是等同于「双向」? + +![](https://labuladong.online/algo/images/图/3.jpeg) + +如果连接无向图中的节点 `x` 和 `y`,把 `matrix[x][y]` 和 `matrix[y][x]` 都变成 `true` 不就行了;邻接表也是类似的操作,在 `x` 的邻居列表里添加 `y`,同时在 `y` 的邻居列表里添加 `x`。 + +把上面的技巧合起来,就变成了无向加权图…… + +好了,关于图的基本介绍就到这里,现在不管来什么乱七八糟的图,你心里应该都有底了。 + +下面来看看所有数据结构都逃不过的问题:遍历。 + +### 图的遍历 + +**[学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/abstraction-of-algorithm/) 说过,各种数据结构被发明出来无非就是为了遍历和访问,所以「遍历」是所有数据结构的基础**。 + +图怎么遍历?还是那句话,参考多叉树,多叉树的 DFS 遍历框架如下: + + +```java +/* 多叉树遍历框架 */ +void traverse(TreeNode root) { + if (root == null) return; + // 前序位置 + for (TreeNode child : root.children) { + traverse(child); + } + // 后序位置 +} +``` + +图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点,而树不会出现这种情况,从某个节点出发必然走到叶子节点,绝不可能回到它自身。 + +所以,如果图包含环,遍历框架就要一个 `visited` 数组进行辅助: + + +```java +// 记录被遍历过的节点 +boolean[] visited; +// 记录从起点到当前节点的路径 +boolean[] onPath; + +/* 图遍历框架 */ +void traverse(Graph graph, int s) { + if (visited[s]) return; + // 经过节点 s,标记为已遍历 + visited[s] = true; + // 做选择:标记节点 s 在路径上 + onPath[s] = true; + for (int neighbor : graph.neighbors(s)) { + traverse(graph, neighbor); + } + // 撤销选择:节点 s 离开路径 + onPath[s] = false; +} +``` + +注意 `visited` 数组和 `onPath` 数组的区别,因为二叉树算是特殊的图,所以用遍历二叉树的过程来理解下这两个数组的区别: + +![](https://labuladong.online/algo/images/迭代遍历二叉树/1.gif) + +**上述 GIF 描述了递归遍历二叉树的过程,在 `visited` 中被标记为 true 的节点用灰色表示,在 `onPath` 中被标记为 true 的节点用绿色表示**,类比贪吃蛇游戏,`visited` 记录蛇经过过的格子,而 `onPath` 仅仅记录蛇身。在图的遍历过程中,`onPath` 用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景,这下你可以理解它们二者的区别了吧。 + +如果让你处理路径相关的问题,这个 `onPath` 变量是肯定会被用到的,比如 [拓扑排序](https://labuladong.online/algo/data-structure/topological-sort/) 中就有运用。 + +另外,你应该注意到了,这个 `onPath` 数组的操作很像前文 [回溯算法核心套路](https://labuladong.online/algo/essential-technique/backtrack-framework/) 中做「做选择」和「撤销选择」,区别在于位置:回溯算法的「做选择」和「撤销选择」在 for 循环里面,而对 `onPath` 数组的操作在 for 循环外面。 + +为什么有这个区别呢?这就是前文 [东哥带你刷二叉树(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 中讲到的回溯算法和 DFS 算法的区别所在:回溯算法关注的不是节点,而是树枝。如果没印象了,强烈建议重新阅读前文。 + +对于回溯算法,我们需要在「树枝」上做选择和撤销选择: + +![](https://labuladong.online/algo/images/backtracking/5.jpg) + +他们的区别可以这样反映到代码上: + + +```java +// DFS 算法,关注点在节点 +void traverse(TreeNode root) { + if (root == null) return; + printf("进入节点 %s", root); + for (TreeNode child : root.children) { + traverse(child); + } + printf("离开节点 %s", root); +} + +// 回溯算法,关注点在树枝 +void backtrack(TreeNode root) { + if (root == null) return; + for (TreeNode child : root.children) { + // 做选择 + printf("从 %s 到 %s", root, child); + backtrack(child); + // 撤销选择 + printf("从 %s 到 %s", child, root); + } +} +``` + +如果执行这段代码,你会发现根节点被漏掉了: + + +```java +void traverse(TreeNode root) { + if (root == null) return; + for (TreeNode child : root.children) { + printf("进入节点 %s", child); + traverse(child); + printf("离开节点 %s", child); + } +} +``` + +所以对于这里「图」的遍历,我们应该用 DFS 算法,即把 `onPath` 的操作放到 for 循环外面,否则会漏掉记录起始点的遍历。 + +说了这么多 `onPath` 数组,再说下 `visited` 数组,其目的很明显了,由于图可能含有环,`visited` 数组就是防止递归重复遍历同一个节点进入死循环的。 + +当然,如果题目告诉你图中不含环,可以把 `visited` 数组都省掉,基本就是多叉树的遍历。 + +### 题目实践 + +下面我们来看力扣第 797 题「所有可能路径」,函数签名如下: + +```java +List> allPathsSourceTarget(int[][] graph); +``` + +题目输入一幅**有向无环图**,这个图包含 `n` 个节点,标号为 `0, 1, 2,..., n - 1`,请你计算所有从节点 `0` 到节点 `n - 1` 的路径。 + +输入的这个 `graph` 其实就是「邻接表」表示的一幅图,`graph[i]` 存储这节点 `i` 的所有邻居节点。 + +比如输入 `graph = [[1,2],[3],[3],[]]`,就代表下面这幅图: + +![](https://labuladong.online/algo/images/图/1.jpg) + +算法应该返回 `[[0,1,3],[0,2,3]]`,即 `0` 到 `3` 的所有路径。 + +**解法很简单,以 `0` 为起点遍历图,同时记录遍历过的路径,当遍历到终点时将路径记录下来即可**。 + +既然输入的图是无环的,我们就不需要 `visited` 数组辅助了,直接套用图的遍历框架: + + +```java +class Solution { + // 记录所有路径 + List> res = new LinkedList<>(); + + public List> allPathsSourceTarget(int[][] graph) { + // 维护递归过程中经过的路径 + LinkedList path = new LinkedList<>(); + traverse(graph, 0, path); + return res; + } + + /* 图的遍历框架 */ + void traverse(int[][] graph, int s, LinkedList path) { + // 添加节点 s 到路径 + path.addLast(s); + + int n = graph.length; + if (s == n - 1) { + // 到达终点 + res.add(new LinkedList<>(path)); + // 可以在这直接 return,但要 removeLast 正确维护 path + // path.removeLast(); + // return; + // 不 return 也可以,因为图中不包含环,不会出现无限递归 + } + + // 递归每个相邻节点 + for (int v : graph[s]) { + traverse(graph, v, path); + } + + // 从路径移出节点 s + path.removeLast(); + } +} +``` + + + +这道题就这样解决了,注意 Java 的语言特性,因为 Java 函数参数传的是对象引用,所以向 `res` 中添加 `path` 时需要拷贝一个新的列表,否则最终 `res` 中的列表都是空的。 + +最后总结一下,图的存储方式主要有邻接表和邻接矩阵,无论什么花里胡哨的图,都可以用这两种方式存储。 + +在笔试中,最常考的算法是图的遍历,和多叉树的遍历框架是非常类似的。 + +当然,图还会有很多其他的有趣算法,比如 [二分图判定](https://labuladong.online/algo/data-structure/bipartite-graph/),[环检测和拓扑排序](https://labuladong.online/algo/data-structure/topological-sort/)(编译器循环引用检测就是类似的算法),[最小生成树](https://labuladong.online/algo/data-structure/kruskal/),[Dijkstra 最短路径算法](https://labuladong.online/algo/data-structure/dijkstra/) 等等,有兴趣的读者可以去看看,本文就到这了。 + + + +
+
+引用本文的文章 + + - [Dijkstra 算法模板及应用](https://labuladong.online/algo/data-structure/dijkstra/) + - [Prim 最小生成树算法](https://labuladong.online/algo/data-structure/prim/) + - [【强化练习】利用后序位置解题 II](https://labuladong.online/algo/problem-set/binary-tree-post-order-2/) + - [【强化练习】利用后序位置解题 III](https://labuladong.online/algo/problem-set/binary-tree-post-order-3/) + - [【强化练习】运用层序遍历解题 II](https://labuladong.online/algo/problem-set/binary-tree-level-2/) + - [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) + - [东哥带你刷二叉树(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + - [二分图判定算法](https://labuladong.online/algo/data-structure/bipartite-graph/) + - [二叉树基础及常见类型](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) + - [众里寻他千百度:名流问题](https://labuladong.online/algo/frequency-interview/find-celebrity/) + - [前缀树算法模板秒杀五道算法题](https://labuladong.online/algo/data-structure/trie/) + - [动态数组代码实现](https://labuladong.online/algo/data-structure-basic/array-implement/) + - [回溯算法解题套路框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) + - [并查集(Union-Find)算法](https://labuladong.online/algo/data-structure/union-find/) + - [我的刷题心得:算法的本质](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [环检测及拓扑排序算法](https://labuladong.online/algo/data-structure/topological-sort/) + - [用算法打败算法](https://labuladong.online/algo/other-skills/algorithm-in-pdf/) + - [算法学习和心流体验](https://labuladong.online/algo/other-skills/hert-flow/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | +| :----: | :----: | +| [1245. Tree Diameter](https://leetcode.com/problems/tree-diameter/?show=1)🔒 | [1245. 树的直径](https://leetcode.cn/problems/tree-diameter/?show=1)🔒 | +| [133. Clone Graph](https://leetcode.com/problems/clone-graph/?show=1) | [133. 克隆图](https://leetcode.cn/problems/clone-graph/?show=1) | +| [1443. Minimum Time to Collect All Apples in a Tree](https://leetcode.com/problems/minimum-time-to-collect-all-apples-in-a-tree/?show=1) | [1443. 收集树上所有苹果的最少时间](https://leetcode.cn/problems/minimum-time-to-collect-all-apples-in-a-tree/?show=1) | +| [200. Number of Islands](https://leetcode.com/problems/number-of-islands/?show=1) | [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/?show=1) | +| [2049. Count Nodes With the Highest Score](https://leetcode.com/problems/count-nodes-with-the-highest-score/?show=1) | [2049. 统计最高分的节点数目](https://leetcode.cn/problems/count-nodes-with-the-highest-score/?show=1) | +| [310. Minimum Height Trees](https://leetcode.com/problems/minimum-height-trees/?show=1) | [310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/?show=1) | +| [542. 01 Matrix](https://leetcode.com/problems/01-matrix/?show=1) | [542. 01 矩阵](https://leetcode.cn/problems/01-matrix/?show=1) | +| [582. Kill Process](https://leetcode.com/problems/kill-process/?show=1)🔒 | [582. 杀掉进程](https://leetcode.cn/problems/kill-process/?show=1)🔒 | +| [79. Word Search](https://leetcode.com/problems/word-search/?show=1) | [79. 单词搜索](https://leetcode.cn/problems/word-search/?show=1) | +| - | [剑指 Offer II 110. 所有路径](https://leetcode.cn/problems/bP4bmD/?show=1) | + +
+
+ + + +**_____________** + +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" index dfc9094a4a..b309b73833 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" @@ -1,8 +1,33 @@ -# 拆解复杂问题:实现计算器 +# 拓展:如何实现一个计算器 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [224. Basic Calculator](https://leetcode.com/problems/basic-calculator/) | [224. 基本计算器](https://leetcode.cn/problems/basic-calculator/) | 🔴 | +| [227. Basic Calculator II](https://leetcode.com/problems/basic-calculator-ii/) | [227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/) | 🟠 | +| [772. Basic Calculator III](https://leetcode.com/problems/basic-calculator-iii/)🔒 | [772. 基本计算器 III](https://leetcode.cn/problems/basic-calculator-iii/)🔒 | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [队列/栈的原理](https://labuladong.online/algo/data-structure-basic/queue-stack-basic/) 我们最终要实现的计算器功能如下: - -1、输入一个字符串,可以包含`+ - * /`、数字、括号以及空格,你的算法返回运算结构。 + +1、输入一个字符串,可以包含 `+ - * /`、数字、括号以及空格,你的算法返回运算结果。 2、要符合运算法则,括号的优先级最高,先乘除后加减。 @@ -12,7 +37,12 @@ 比如输入如下字符串,算法会返回 9: -`3 * (2-6 /(3 -7))` +```java + 3 * (2 - 6 / (3 - 7)) += 3 * (2 - 6 / (-4)) += 3 * (2 - (-1)) += 9 +``` 可以看到,这就已经非常接近我们实际生活中使用的计算器了,虽然我们以前肯定都用过计算器,但是如果简单思考一下其算法实现,就会大惊失色: @@ -24,56 +54,57 @@ 我记得很多大学数据结构的教材上,在讲栈这种数据结构的时候,应该都会用计算器举例,但是有一说一,讲的真的垃圾,不知道多少未来的计算机科学家就被这种简单的数据结构劝退了。 -那么本文就来聊聊怎么实现上述一个功能完备的计算器功能,**关键在于层层拆解问题,化整为零,逐个击破**,相信这种思维方式能帮大家解决各种复杂问题。 +那么本文就来聊聊怎么实现上述一个功能完备的计算器功能,**关键在于层层拆解问题,化整为零,逐个击破**,几条简单的算法规则就可以处理极其复杂的运算,相信这种思维方式能帮大家解决各种复杂问题。 下面就来拆解,从最简单的一个问题开始。 -### 一、字符串转整数 +## 一、字符串转整数 是的,就是这么一个简单的问题,首先告诉我,怎么把一个字符串形式的**正**整数,转化成 int 型? -```cpp -string s = "458"; +```java +String s = "458"; int n = 0; -for (int i = 0; i < s.size(); i++) { - char c = s[i]; +for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); n = 10 * n + (c - '0'); } // n 现在就等于 458 ``` -这个还是很简单的吧,老套路了。但是即便这么简单,依然有坑:**`(c - '0')`的这个括号不能省略,否则可能造成整型溢出**。 +这个还是很简单的吧,老套路了。但是即便这么简单,依然有坑:**`(c - '0')` 的这个括号不能省略,否则可能造成整型溢出**。 -因为变量`c`是一个 ASCII 码,如果不加括号就会先加后减,想象一下`s`如果接近 INT_MAX,就会溢出。所以用括号保证先减后加才行。 +因为变量 `c` 是一个 ASCII 码,如果不加括号就会先加后减,想象一下 `s` 如果接近 INT_MAX,就会溢出。所以用括号保证先减后加才行。 -### 二、处理加减法 +## 二、处理加减法 -现在进一步,**如果输入的这个算式只包含加减法,而且不存在空格**,你怎么计算结果?我们拿字符串算式`1-12+3`为例,来说一个很简单的思路: +现在进一步,**如果输入的这个算式只包含加减法,而且不存在空格**,你怎么计算结果?我们拿字符串算式 `1-12+3` 为例,来说一个很简单的思路: -1、先给第一个数字加一个默认符号`+`,变成`+1-12+3`。 +1、先给第一个数字加一个默认符号 `+`,变成 `+1-12+3`。 -2、把一个运算符和数字组合成一对儿,也就是三对儿`+1`,`-12`,`+3`,把它们转化成数字,然后放到一个栈中。 +2、把一个运算符和数字组合成一对儿,也就是三对儿 `+1`,`-12`,`+3`,把它们转化成数字,然后放到一个栈中。 3、将栈中所有的数字求和,就是原算式的结果。 我们直接看代码,结合一张图就看明白了: -```cpp -int calculate(string s) { - stack stk; +```java +int calculate(String s) { + Stack stk = new Stack<>(); // 记录算式中的数字 int num = 0; // 记录 num 前的符号,初始化为 + char sign = '+'; - for (int i = 0; i < s.size(); i++) { - char c = s[i]; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); // 如果是数字,连续读取到 num - if (isdigit(c)) + if (Character.isDigit(c)) { num = 10 * num + (c - '0'); - // 如果不是数字,就是遇到了下一个符号, - // 之前的数字和符号就要存进栈中 - if (!isdigit(c) || i == s.size() - 1) { + } + // 如果不是数字,就是遇到了下一个符号,或者是算式的末尾 + // 那么之前的数字和符号就要存进栈中 + if (c == '+' || c == '-' || i == s.length() - 1) { switch (sign) { case '+': stk.push(num); break; @@ -87,195 +118,273 @@ int calculate(string s) { } // 将栈中所有结果求和就是答案 int res = 0; - while (!stk.empty()) { - res += stk.top(); - stk.pop(); + while (!stk.isEmpty()) { + res += stk.pop(); } return res; } ``` -我估计就是中间带`switch`语句的部分有点不好理解吧,`i`就是从左到右扫描,`sign`和`num`跟在它身后。当`s[i]`遇到一个运算符时,情况是这样的: +我估计就是中间带 `switch` 语句的部分有点不好理解吧,`i` 就是从左到右扫描,`sign` 和 `num` 跟在它身后。当 `s[i]` 遇到一个运算符时,情况是这样的: -![](../pictures/calculator/1.jpg) +![](https://labuladong.online/algo/images/calculator/1.jpg) -所以说,此时要根据`sign`的 case 不同选择`nums`的正负号,存入栈中,然后更新`sign`并清零`nums`记录下一对儿符合和数字的组合。 +所以说,此时要根据 `sign` 的 case 不同选择 `nums` 的正负号,存入栈中,然后更新 `sign` 并清零 `nums` 记录下一对儿符合和数字的组合。 -另外注意,不只是遇到新的符号会触发入栈,当`i`走到了算式的尽头(`i == s.size() - 1`),也应该将前面的数字入栈,方便后续计算最终结果。 +另外注意,不只是遇到新的符号会触发入栈,当 `i` 走到了算式的尽头(`i == s.size() - 1` ),也应该将前面的数字入栈,方便后续计算最终结果。 -![](../pictures/calculator/2.jpg) +![](https://labuladong.online/algo/images/calculator/2.jpg) 至此,仅处理紧凑加减法字符串的算法就完成了,请确保理解以上内容,后续的内容就基于这个框架修修改改就完事儿了。 -### 三、处理乘除法 +## 三、处理乘除法 -其实思路跟仅处理加减法没啥区别,拿字符串`2-3*4+5`举例,核心思路依然是把字符串分解成符号和数字的组合。 +其实思路跟仅处理加减法没啥区别,拿字符串 `2-3*4+5` 举例,核心思路依然是把字符串分解成符号和数字的组合。 -比如上述例子就可以分解为`+2`,`-3`,`*4`,`+5`几对儿,我们刚才不是没有处理乘除号吗,很简单,**其他部分都不用变**,在`switch`部分加上对应的 case 就行了: +比如上述例子就可以分解为 `+2`,`-3`,`*4`,`+5` 几对儿,我们刚才不是没有处理乘除号吗,很简单,**其他部分都不用变**,在 `switch` 部分加上对应的 case 就行了: -```cpp -for (int i = 0; i < s.size(); i++) { - char c = s[i]; - if (isdigit(c)) - num = 10 * num + (c - '0'); +```java +int calculate(String s) { + Stack stk = new Stack<>(); + // 记录算式中的数字 + int num = 0; + // 记录 num 前的符号,初始化为 + + char sign = '+'; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isDigit(c)) { + num = 10 * num + (c - '0'); + } - if (!isdigit(c) || i == s.size() - 1) { - switch (sign) { + if (c == '+' || c == '-' || c == '/' || c == '*' || i == s.length() - 1) { int pre; - case '+': - stk.push(num); break; - case '-': - stk.push(-num); break; - // 只要拿出前一个数字做对应运算即可 - case '*': - pre = stk.top(); - stk.pop(); - stk.push(pre * num); - break; - case '/': - pre = stk.top(); - stk.pop(); - stk.push(pre / num); - break; + switch (sign) { + case '+': + stk.push(num); break; + case '-': + stk.push(-num); break; + // 只要拿出前一个数字做对应运算即可 + case '*': + pre = stk.pop(); + stk.push(pre * num); + break; + case '/': + pre = stk.pop(); + stk.push(pre / num); + break; + } + // 更新符号为当前符号,数字清零 + sign = c; + num = 0; } - // 更新符号为当前符号,数字清零 - sign = c; - num = 0; } + // 将栈中所有结果求和就是答案 + int res = 0; + while (!stk.isEmpty()) { + res += stk.pop(); + } + return res; } ``` -![](../pictures/calculator/3.jpg) +![](https://labuladong.online/algo/images/calculator/3.jpg) + + **乘除法优先于加减法体现在,乘除法可以和栈顶的数结合,而加减法只能把自己放入栈**。 -现在我们思考一下**如何处理字符串中可能出现的空格字符**。其实也非常简单,想想空格字符的出现,会影响我们现有代码的哪一部分? +现在我们思考一下如何处理字符串中可能出现的空格字符。其实按照目前的代码,我们根本不用特殊处理空格字符,你注意 if 条件,当字符 `c` 是空格时,不会对它做任何处理,直接跳过了。 + +好了,我们现在的算法已经可以按照正确的法则计算加减乘除,并且自动忽略空格符,剩下的就是如何让算法正确识别括号了。 + + + -```cpp -// 如果 c 非数字 -if (!isdigit(c) || i == s.size() - 1) { - switch (c) {...} - sign = c; - num = 0; -} -``` -显然空格会进入这个 if 语句,但是我们并不想让空格的情况进入这个 if,因为这里会更新`sign`并清零`nums`,空格根本就不是运算符,应该被忽略。 -那么只要多加一个条件即可: -```cpp -if ((!isdigit(c) && c != ' ') || i == s.size() - 1) { - ... +## 四、处理括号 + +处理算式中的括号看起来应该是最难的,但真没有看起来那么难。我们先把上面的代码稍微改一下: + +```java +int calculate(String s) { + return _calculate(s, 0, s.length() - 1); } -``` -好了,现在我们的算法已经可以按照正确的法则计算加减乘除,并且自动忽略空格符,剩下的就是如何让算法正确识别括号了。 - -### 四、处理括号 - -处理算式中的括号看起来应该是最难的,但真没有看起来那么难。 - -为了规避编程语言的繁琐细节,我把前面解法的代码翻译成 Python 版本: - -```python -def calculate(s: str) -> int: - - def helper(s: List) -> int: - stack = [] - sign = '+' - num = 0 - - while len(s) > 0: - c = s.pop(0) - if c.isdigit(): - num = 10 * num + int(c) - - if (not c.isdigit() and c != ' ') or len(s) == 0: - if sign == '+': - stack.append(num) - elif sign == '-': - stack.append(-num) - elif sign == '*': - stack[-1] = stack[-1] * num - elif sign == '/': - # python 除法向 0 取整的写法 - stack[-1] = int(stack[-1] / float(num)) - num = 0 - sign = c - - return sum(stack) - # 需要把字符串转成列表方便操作 - return helper(list(s)) +// 定义:返回 s[start..end] 内的表达式的计算结果 +int _calculate(String s, int start, int end) { + // 需要把字符串转成双端队列方便操作 + Stack stk = new Stack<>(); + // 记录算式中的数字 + int num = 0; + // 记录 num 前的符号,初始化为 + + char sign = '+'; + for (int i = start; i <= end; i++) { + char c = s.charAt(i); + if (Character.isDigit(c)) { + num = 10 * num + (c - '0'); + } + + if (c == '+' || c == '-' || c == '/' || c == '*' || i == s.length() - 1) { + int pre; + switch (sign) { + case '+': + stk.push(num); + break; + case '-': + stk.push(-num); + break; + // 只要拿出前一个数字做对应运算即可 + case '*': + pre = stk.pop(); + stk.push(pre * num); + break; + case '/': + pre = stk.pop(); + stk.push(pre / num); + break; + } + // 更新符号为当前符号,数字清零 + sign = c; + num = 0; + } + } + // 将栈中所有结果求和就是答案 + int res = 0; + while (!stk.isEmpty()) { + res += stk.pop(); + } + return res; +} ``` -这段代码跟刚才 C++ 代码完全相同,唯一的区别是,不是从左到右遍历字符串,而是不断从左边`pop`出字符,本质还是一样的。 +这里我们定义了一个新的函数 `_calculate`,它接受三个参数,分别是字符串 `s`,以及字符串的左右边界 `start` 和 `end`。这样我们就可以计算 `s` 中任意一个子表达式的值了。 -那么,为什么说处理括号没有看起来那么难呢,**因为括号具有递归性质**。我们拿字符串`3*(4-5/2)-6`举例: +那么,为什么说处理括号没有看起来那么难呢,**因为括号具有递归性质**。我们拿字符串 `3*(4-5/2)-6` 举例: -calculate(`3*(4-5/2)-6`) -= 3 * calculate(`4-5/2`) - 6 +```java +calculate(3 * (4 - 5/2) - 6) += 3 * calculate(4 - 5/2) - 6 = 3 * 2 - 6 = 0 +``` + +可以脑补一下,无论多少层括号嵌套,通过 `_calculate` 函数递归调用自己,都可以将括号中的算式算出结果。**换句话说,括号包含的算式,我们直接视为一个数字就行了**。 + +那么现在的问题是,如果我遇到一个左括号 `(`,我怎么知道这个括号对应的右括号 `)` 在哪里呢?这就又要用到栈了,我们可以对 `s` 进行预计算,提前找出每个左括号对应的右括号的位置。 + +具体看代码吧,基于上面的 `_calculate` 函数,我们再添加一些逻辑: + +```java +class Solution { + public int calculate(String s) { + // key 是左括号的索引,value 是对应的右括号的索引 + Map rightIndex = new HashMap<>(); + // 利用栈结构来找到对应的括号 + Stack stack = new Stack<>(); + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + stack.push(i); + } else if (s.charAt(i) == ')') { + rightIndex.put(stack.pop(), i); + } + } + return _calculate(s, 0, s.length() - 1, rightIndex); + } -可以脑补一下,无论多少层括号嵌套,通过 calculate 函数递归调用自己,都可以将括号中的算式化简成一个数字。**换句话说,括号包含的算式,我们直接视为一个数字就行了**。 - -现在的问题是,递归的开始条件和结束条件是什么?**遇到`(`开始递归,遇到`)`结束递归**: - -```python -def calculate(s: str) -> int: - - def helper(s: List) -> int: - stack = [] - sign = '+' - num = 0 - - while len(s) > 0: - c = s.pop(0) - if c.isdigit(): - num = 10 * num + int(c) - # 遇到左括号开始递归计算 num - if c == '(': - num = helper(s) - - if (not c.isdigit() and c != ' ') or len(s) == 0: - if sign == '+': ... - elif sign == '-': ... - elif sign == '*': ... - elif sign == '/': ... - num = 0 - sign = c - # 遇到右括号返回递归结果 - if c == ')': break - return sum(stack) - - return helper(list(s)) + // 定义:返回 s[start..end] 内的表达式的计算结果 + private int _calculate(String s, int start, int end, Map rightIndex) { + // 需要把字符串转成双端队列方便操作 + Stack stk = new Stack<>(); + // 记录算式中的数字 + int num = 0; + // 记录 num 前的符号,初始化为 + + char sign = '+'; + for (int i = start; i <= end; i++) { + char c = s.charAt(i); + if (Character.isDigit(c)) { + num = 10 * num + (c - '0'); + } + if (c == '(') { + // 递归计算括号内的表达式 + num = _calculate(s, i + 1, rightIndex.get(i) - 1, rightIndex); + i = rightIndex.get(i); + } + if (c == '+' || c == '-' || c == '*' || c == '/' || i == end) { + int pre; + switch (sign) { + case '+': + stk.push(num); + break; + case '-': + stk.push(-num); + break; + // 只要拿出前一个数字做对应运算即可 + case '*': + pre = stk.pop(); + stk.push(pre * num); + break; + case '/': + pre = stk.pop(); + stk.push(pre / num); + break; + } + // 更新符号为当前符号,数字清零 + sign = c; + num = 0; + } + } + // 将栈中所有结果求和就是答案 + int res = 0; + while (!stk.isEmpty()) { + res += stk.pop(); + } + return res; + } +} ``` -![](../pictures/calculator/4.jpg) +![](https://labuladong.online/algo/images/calculator/4.jpg) + +![](https://labuladong.online/algo/images/calculator/5.jpg) + +![](https://labuladong.online/algo/images/calculator/6.jpg) -![](../pictures/calculator/5.jpg) -![](../pictures/calculator/6.jpg) 你看,加了两三行代码,就可以处理括号了,这就是递归的魅力。至此,计算器的全部功能就实现了,通过对问题的层层拆解化整为零,再回头看,这个问题似乎也没那么复杂嘛。 -### 五、最后总结 +## 五、最后总结 本文借实现计算器的问题,主要想表达的是一种处理复杂问题的思路。 我们首先从字符串转数字这个简单问题开始,进而处理只包含加减法的算式,进而处理包含加减乘除四则运算的算式,进而处理空格字符,进而处理包含括号的算式。 -**可见,对于一些比较困难的问题,其解法并不是一蹴而就的,而是步步推进,螺旋上升的**。如果一开始给你原题,你不会做,甚至看不懂答案,都很正常,关键在于我们自己如何简化问题,如何以退为进。 +可见,对于一些比较困难的问题,其解法并不是一蹴而就的,而是步步推进螺旋上升的。如果一开始给你原题,你不会做,甚至看不懂答案,都很正常,关键在于我们自己如何简化问题,如何以退为进。 + +搞清楚计算器算法原理后,**我们最终实现的这个全能的计算器代码可以保存下来**,一些其他算法问题可能会要求你计算表达式的值,到时候可以把这个类套出来直接用,不用自己从头写了。 + + + + + + + +
+
+引用本文的文章 + + - [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + +

+ + -**退而求其次是一种很聪明策略**。你想想啊,假设这是一道考试题,你不会实现这个计算器,但是你写了字符串转整数的算法并指出了容易溢出的陷阱,那起码可以得 20 分吧;如果你能够处理加减法,那可以得 40 分吧;如果你能处理加减乘除四则运算,那起码够 70 分了;再加上处理空格字符,80 有了吧。我就是不会处理括号,那就算了,80 已经很 OK 了好不好。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:常用的位操作](../算法思维系列/常用的位操作.md) -[下一篇:烧饼排序](../算法思维系列/烧饼排序.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" new file mode 100644 index 0000000000..c90875efae --- /dev/null +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" @@ -0,0 +1,648 @@ +# 环检测及拓扑排序算法 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [207. Course Schedule](https://leetcode.com/problems/course-schedule/) | [207. 课程表](https://leetcode.cn/problems/course-schedule/) | 🟠 | +| [210. Course Schedule II](https://leetcode.com/problems/course-schedule-ii/) | [210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [图结构基础及通用实现](https://labuladong.online/algo/data-structure-basic/graph-basic/) +> - [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) + +> [!IMPORTANT] +> 进行拓扑排序之前,先要确保图中没有环。 +> +> 图的「逆后序遍历」顺序,就是拓扑排序的结果。 + +图这种数据结构有一些比较特殊的算法,比如二分图判断,有环图无环图的判断,拓扑排序,以及最经典的最小生成树,单源最短路径问题,更难的就是类似网络流这样的问题。 + +不过目前来看,像网络流这种问题,你又不是打竞赛的,没时间的话就没必要学了;像 [最小生成树](https://labuladong.online/algo/data-structure/prim/) 和 [最短路径问题](https://labuladong.online/algo/data-structure/dijkstra/),虽然从刷题的角度遇到的不多,但它们属于经典算法,学有余力可以掌握一下;像 [二分图判定](https://labuladong.online/algo/data-structure/bipartite-graph/)、拓扑排序这一类,本质上就是图的遍历,属于比较基本的算法,应该熟练地掌握。 + +**那么本文就结合具体的算法题,来说两个图论算法:有向图的环检测、拓扑排序算法**。 + +这两个算法既可以用 DFS 思路解决,也可以用 BFS 思路解决,相对而言 BFS 解法从代码实现上看更简洁一些,但 DFS 解法有助于你进一步理解递归遍历数据结构的奥义,所以本文中我先讲 DFS 遍历的思路,再讲 BFS 遍历的思路。 + +## 环检测算法(DFS 版本) + +先来看看力扣第 207 题「课程表」: + + + + + + + +```java +// 函数签名如下 +boolean canFinish(int numCourses, int[][] prerequisites); +``` + +题目应该不难理解,什么时候无法修完所有课程?当存在循环依赖的时候。 + +其实这种场景在现实生活中也十分常见,比如我们写代码 import 包也是一个例子,必须合理设计代码目录结构,否则会出现循环依赖,编译器会报错,所以编译器实际上也使用了类似算法来判断你的代码是否能够成功编译。 + +**看到依赖问题,首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖**。 + +具体来说,我们首先可以把课程看成「有向图」中的节点,节点编号分别是 `0, 1, ..., numCourses-1`,把课程之间的依赖关系看做节点之间的有向边。 + +比如说必须修完课程 `1` 才能去修课程 `3`,那么就有一条有向边从节点 `1` 指向 `3`。 + +所以我们可以根据题目输入的 `prerequisites` 数组生成一幅类似这样的图: + +![](https://labuladong.online/algo/images/topological-sort/1.jpeg) + +**如果发现这幅有向图中存在环,那就说明课程之间存在循环依赖,肯定没办法全部上完;反之,如果没有环,那么肯定能上完全部课程**。 + +好,那么想解决这个问题,首先我们要把题目的输入转化成一幅有向图,然后再判断图中是否存在环。 + +如何转换成图呢?我们前文 [图结构的存储](https://labuladong.online/algo/data-structure-basic/graph-basic/) 写过图的两种存储形式,邻接矩阵和邻接表。 + +这里我就用邻接表形式存储图吧,首先可以写一个建图函数: + +```java +List[] buildGraph(int numCourses, int[][] prerequisites) { + // 图中共有 numCourses 个节点 + List[] graph = new LinkedList[numCourses]; + for (int i = 0; i < numCourses; i++) { + graph[i] = new LinkedList<>(); + } + for (int[] edge : prerequisites) { + int from = edge[1], to = edge[0]; + // 添加一条从 from 指向 to 的有向边 + // 边的方向是「被依赖」关系,即修完课程 from 才能修课程 to + graph[from].add(to); + } + return graph; +} +``` + +图建出来了,怎么判断图中有没有环呢? + +很简单,无非就是想考你如何遍历图中的所有路径嘛,如果我能遍历所有路径,那么路径是否成环不就容易算出来了吗? + +[图的 DFS/BFS 遍历基础](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) 写了如何用 DFS 算法遍历图的所有路径,如果忘记了请去复习,下面要用到。 + +我这里先直接套用遍历所有路径的 DFS 代码模板,用一个 `hasCycle` 变量记录是否存在环,**当重复遍历到 `onPath` 中的节点时,就说明遇到了环,设置 `hasCycle = true`**。 + +基于这个思路,先看第一版代码(会超时): + +```java +class Solution { + // 记录递归堆栈中的节点 + boolean[] onPath; + // 记录图中是否有环 + boolean hasCycle = false; + + public boolean canFinish(int numCourses, int[][] prerequisites) { + List[] graph = buildGraph(numCourses, prerequisites); + + onPath = new boolean[numCourses]; + + for (int i = 0; i < numCourses; i++) { + // 遍历图中的所有节点 + traverse(graph, i); + } + // 只要没有循环依赖可以完成所有课程 + return !hasCycle; + } + + // 图遍历函数,遍历所有路径 + void traverse(List[] graph, int s) { + if (hasCycle) { + // 如果已经找到了环,也不用再遍历了 + return; + } + + if (onPath[s]) { + // s 已经在递归路径上,说明成环了 + hasCycle = true; + return; + } + + // 前序代码位置 + onPath[s] = true; + for (int t : graph[s]) { + traverse(graph, t); + } + // 后序代码位置 + onPath[s] = false; + } + + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 代码见前文 + } +} +``` + +注意图中并不是所有节点都相连,所以要用一个 for 循环将所有节点都作为起点调用一次 DFS 搜索算法。 + +其实这个解法已经是正确的了,因为遍历了所有路径,一定可以判定是否成环。但是这个解法无法通过所有测试用例,会超时。那么原因肯定也能猜出来,**有冗余计算**呗。 + +哪里有冗余计算呢?我举个例子你就明白了。 + +假设现在你以节点 `2` 为起点遍历所有可达的路径,最终发现没有环。 + +假设另一个节点 `5` 有一条指向 `2` 的边,你在以 `5` 为起点遍历所有可达的路径时,肯定还会走到 `2`,那么请问,此时你是否还需要继续遍历 `2` 的所有可达路径呢? + +答案是不需要了,因为第一次你没找到环,那么这次也不可能找到环。想明白这里面的冗余计算没有?你如果觉得有反例,可以自己画一下,实际上是没有反例的。 + +那么对症下药就行了:如果我们发现一个节点之前被遍历过,就可以直接跳过,不用再重复遍历了。 + +优化后的代码如下: + +```java +class Solution { + // 记录一次递归堆栈中的节点 + boolean[] onPath; + // 记录节点是否被遍历过 + boolean[] visited; + // 记录图中是否有环 + boolean hasCycle = false; + + public boolean canFinish(int numCourses, int[][] prerequisites) { + List[] graph = buildGraph(numCourses, prerequisites); + + onPath = new boolean[numCourses]; + visited = new boolean[numCourses]; + + for (int i = 0; i < numCourses; i++) { + // 遍历图中的所有节点 + traverse(graph, i); + } + // 只要没有循环依赖可以完成所有课程 + return !hasCycle; + } + + // 图遍历函数,遍历所有路径 + void traverse(List[] graph, int s) { + if (hasCycle) { + // 如果已经找到了环,也不用再遍历了 + return; + } + + if (onPath[s]) { + // s 已经在递归路径上,说明成环了 + hasCycle = true; + return; + } + + if (visited[s]) { + // 不用再重复遍历已遍历过的节点 + return; + } + + // 前序代码位置 + visited[s] = true; + onPath[s] = true; + for (int t : graph[s]) { + traverse(graph, t); + } + // 后序代码位置 + onPath[s] = false; + } + + + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 代码见前文 + } +} +``` + + + +`visited` 为 true 的节点为绿色,`onPath` 为 true 的节点为橙色。 + +你可以打开可视化面板,多次点击 if (onPath[s]) 这部分代码,即可查看 DFS 遍历图的过程。 + + + + + +这道题就解决了,核心就是判断一幅有向图中是否存在环。 + +不过如果出题人继续提问,让你不仅要判断是否存在环,还要返回这个环具体有哪些节点,怎么办? + +你可能说,`onPath` 里面为 true 的索引,不就是组成环的节点编号吗? + +不是的,假设从节点 `0` 开始遍历,下图中绿色的节点是递归的路径,它们在 `onPath` 中的值都是 true,但显然成环的节点只是其中的一部分: + +![](https://labuladong.online/algo/images/topological-sort/4.jpeg) + +这个问题大家可以先思考一下,办法肯定有很多啦,我只给出一个常用的解法。 + +> [!NOTE] +> 最简单直接的解法是,在 `boolean[] onPath` 数组的基础上,我们再使用一个 `Stack path` 栈,把遍历过程中经过的节点顺序也保存下来。 +> +> 比如按照上图绿色的遍历顺序,`path` 从栈底到栈顶的元素就是 `[0,4,5,9,8,7,6]`。此时又一次遇到了节点 `5`,那么就可以知道 `[5,9,8,7,6]` 这部分是环了。 + +接下来,我们来再讲一个经典的图算法:拓扑排序。 + +## 拓扑排序算法(DFS 版本) + +看下力扣第 210 题「课程表 II」: + + + +这道题就是上道题的进阶版,不是仅仅让你判断是否可以完成所有课程,而是进一步让你返回一个合理的上课顺序,保证开始修每个课程时,前置的课程都已经修完。 + +函数签名如下: + +```java +int[] findOrder(int numCourses, int[][] prerequisites); +``` + +这里我先说一下拓扑排序(Topological Sorting)这个名词,网上搜出来的定义很数学,这里干脆用百度百科的一幅图来让你直观地感受下: + +![](https://labuladong.online/algo/images/topological-sort/top.jpg) + +**直观地说就是,让你把一幅图「拉平」,而且这个「拉平」的图里面,所有箭头方向都是一致的**,比如上图所有箭头都是朝右的。 + +很显然,如果一幅有向图中存在环,是无法进行拓扑排序的,因为肯定做不到所有箭头方向一致;反过来,如果一幅图是「有向无环图」,那么一定可以进行拓扑排序。 + +但是我们这道题和拓扑排序有什么关系呢? + +**其实也不难看出来,如果把课程抽象成节点,课程之间的依赖关系抽象成有向边,那么这幅图的拓扑排序结果就是上课顺序**。 + +首先,我们先判断一下题目输入的课程依赖是否成环,成环的话是无法进行拓扑排序的,所以我们可以复用上一道题的主函数: + +```java +public int[] findOrder(int numCourses, int[][] prerequisites) { + if (!canFinish(numCourses, prerequisites)) { + // 不可能完成所有课程 + return new int[]{}; + } + // ... +} +``` + +那么关键问题来了,如何进行拓扑排序?是不是又要秀什么高大上的技巧了? + +**其实特别简单,把图结构后序遍历的结果进行反转,就是拓扑排序的结果**。 + +::: note 需要反转吗? + +有的读者提到,他在网上看到的拓扑排序算法就是后序遍历结果,不用对后序遍历结果进行反转,这是为什么呢? + +你确实可以看到这样的解法,原因是他建图的时候对边的定义和我不同。我建的图中箭头方向是「被依赖」关系,比如节点 `1` 指向 `2`,含义是节点 `1` 被节点 `2` 依赖,即做完 `1` 才能去做 `2`,因为这样更符合我们的直觉。 + +如果你反过来,把有向边定义为「依赖」关系,那么整幅图中边全部反转,就可以不对后序遍历结果反转。具体来说,就是把我的解法代码中 `graph[from].add(to);` 改成 `graph[to].add(from);` 就可以不反转了。 + +::: + +直接看解法代码吧,在上一题环检测的代码基础上添加了记录后序遍历结果的逻辑: + +```java +class Solution { + // 记录后序遍历结果 + List postorder = new ArrayList<>(); + // 记录是否存在环 + boolean hasCycle = false; + boolean[] visited, onPath; + + // 主函数 + public int[] findOrder(int numCourses, int[][] prerequisites) { + List[] graph = buildGraph(numCourses, prerequisites); + visited = new boolean[numCourses]; + onPath = new boolean[numCourses]; + // 遍历图 + for (int i = 0; i < numCourses; i++) { + traverse(graph, i); + } + // 有环图无法进行拓扑排序 + if (hasCycle) { + return new int[]{}; + } + // 逆后序遍历结果即为拓扑排序结果 + Collections.reverse(postorder); + int[] res = new int[numCourses]; + for (int i = 0; i < numCourses; i++) { + res[i] = postorder.get(i); + } + return res; + } + + // 图遍历函数 + void traverse(List[] graph, int s) { + if (onPath[s]) { + // 发现环 + hasCycle = true; + } + if (visited[s] || hasCycle) { + return; + } + // 前序遍历位置 + onPath[s] = true; + visited[s] = true; + for (int t : graph[s]) { + traverse(graph, t); + } + // 后序遍历位置 + postorder.add(s); + onPath[s] = false; + } + + // 建图函数 + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 代码见前文 + } +} +``` + + + +`visited` 为 true 的节点为绿色,`onPath` 为 true 的节点为橙色。 + +你可以打开可视化面板,多次点击 if (onPath[s]) 这部分代码,即可查看 DFS 遍历图的过程。 + + + + + +代码虽然看起来多,但是逻辑应该是很清楚的,只要图中无环,那么我们就调用 `traverse` 函数对图进行 DFS 遍历,记录后序遍历结果,最后把后序遍历结果反转,作为最终的答案。 + +**那么为什么后序遍历的反转结果就是拓扑排序呢**? + +我这里也避免数学证明,用一个直观地例子来解释,我们就说二叉树,这是我们说过很多次的二叉树遍历框架: + +```java +void traverse(TreeNode root) { + // 前序遍历代码位置 + traverse(root.left) + // 中序遍历代码位置 + traverse(root.right) + // 后序遍历代码位置 +} +``` + +二叉树的后序遍历是什么时候?遍历完左右子树之后才会执行后序遍历位置的代码。换句话说,当左右子树的节点都被装到结果列表里面了,根节点才会被装进去。 + +**后序遍历的这一特点很重要,之所以拓扑排序的基础是后序遍历,是因为一个任务必须等到它依赖的所有任务都完成之后才能开始开始执行**。 + +你把二叉树理解成一幅有向图,边的方向是由父节点指向子节点,那么就是下图这样: + +![](https://labuladong.online/algo/images/topological-sort/2.jpeg) + +对于标准的后序遍历结果,根节点出现在最后,只要把遍历结果反过来,就是拓扑排序结果: + +![](https://labuladong.online/algo/images/topological-sort/3.jpeg) + +我知道有读者会问,后序遍历结果反转,和前序遍历结果有什么关系? + +对于二叉树来说你看起来好像有关系,实际上二者没有任何关系。你千万不要认为后序遍历反转的结果等同于前序遍历结果。 + +它俩的关键区别在 [二叉树思想(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 已经讲过了,后序位置的代码是等到左右子树都遍历完才执行的,只有它才能体现出「依赖」关系,其他遍历顺序都做不到。 + + + + + + + + +## 环检测算法(BFS 版本) + +刚才讲了用 DFS 算法利用 `onPath` 数组判断是否存在环;也讲了用 DFS 算法利用逆后序遍历进行拓扑排序。 + +其实 BFS 算法借助 `indegree` 数组记录每个节点的「入度」,也可以实现这两个算法。不熟悉 BFS 算法的读者可阅读前文 [BFS 算法核心框架](https://labuladong.online/algo/essential-technique/bfs-framework/)。 + +所谓「出度」和「入度」是「有向图」中的概念,很直观:如果一个节点 `x` 有 `a` 条边指向别的节点,同时被 `b` 条边所指,则称节点 `x` 的出度为 `a`,入度为 `b`。 + +先说环检测算法,直接看 BFS 的解法代码: + +```java +class Solution { + public boolean canFinish(int numCourses, int[][] prerequisites) { + // 建图,有向边代表「被依赖」关系 + List[] graph = buildGraph(numCourses, prerequisites); + // 构建入度数组 + int[] indegree = new int[numCourses]; + for (int[] edge : prerequisites) { + int from = edge[1], to = edge[0]; + // 节点 to 的入度加一 + indegree[to]++; + } + + // 根据入度初始化队列中的节点 + Queue q = new LinkedList<>(); + for (int i = 0; i < numCourses; i++) { + if (indegree[i] == 0) { + // 节点 i 没有入度,即没有依赖的节点 + // 可以作为拓扑排序的起点,加入队列 + q.offer(i); + } + } + + // 记录遍历的节点个数 + int count = 0; + // 开始执行 BFS 循环 + while (!q.isEmpty()) { + // 弹出节点 cur,并将它指向的节点的入度减一 + int cur = q.poll(); + count++; + for (int next : graph[cur]) { + indegree[next]--; + if (indegree[next] == 0) { + // 如果入度变为 0,说明 next 依赖的节点都已被遍历 + q.offer(next); + } + } + } + + // 如果所有节点都被遍历过,说明不成环 + return count == numCourses; + } + + // 建图函数 + List[] buildGraph(int n, int[][] edges) { + // 见前文 + } +} +``` + +我先总结下这段 BFS 算法的思路: + +1、构建邻接表,和之前一样,边的方向表示「被依赖」关系。 + +2、构建一个 `indegree` 数组记录每个节点的入度,即 `indegree[i]` 记录节点 `i` 的入度。 + +3、对 BFS 队列进行初始化,将入度为 0 的节点首先装入队列。 + +**4、开始执行 BFS 循环,不断弹出队列中的节点,减少相邻节点的入度,并将入度变为 0 的节点加入队列**。 + +**5、如果最终所有节点都被遍历过(`count` 等于节点数),则说明不存在环,反之则说明存在环**。 + +我画个图你就容易理解了,比如下面这幅图,节点中的数字代表该节点的入度: + +![](https://labuladong.online/algo/images/topological-sort/5.jpeg) + +队列进行初始化后,入度为 0 的节点首先被加入队列: + +![](https://labuladong.online/algo/images/topological-sort/6.jpeg) + +开始执行 BFS 循环,从队列中弹出一个节点,减少相邻节点的入度,同时将新产生的入度为 0 的节点加入队列: + +![](https://labuladong.online/algo/images/topological-sort/7.jpeg) + +继续从队列弹出节点,并减少相邻节点的入度,这一次没有新产生的入度为 0 的节点: + +![](https://labuladong.online/algo/images/topological-sort/8.jpeg) + +继续从队列弹出节点,并减少相邻节点的入度,同时将新产生的入度为 0 的节点加入队列: + +![](https://labuladong.online/algo/images/topological-sort/9.jpeg) + +继续弹出节点,直到队列为空: + +![](https://labuladong.online/algo/images/topological-sort/10.jpeg) + +这时候,所有节点都被遍历过一遍,也就说明图中不存在环。 + +反过来说,如果按照上述逻辑执行 BFS 算法,存在节点没有被遍历,则说明成环。 + +比如下面这种情况,队列中最初只有一个入度为 0 的节点: + +![](https://labuladong.online/algo/images/topological-sort/11.jpeg) + +当弹出这个节点并减小相邻节点的入度之后队列为空,但并没有产生新的入度为 0 的节点加入队列,所以 BFS 算法终止: + +![](https://labuladong.online/algo/images/topological-sort/12.jpeg) + +你看到了,如果存在节点没有被遍历,那么说明图中存在环,现在回头去看 BFS 的代码,你应该就很容易理解其中的逻辑了。 + + + + + + + +## 拓扑排序算法(BFS 版本) + +**如果你能看懂 BFS 版本的环检测算法,那么就很容易得到 BFS 版本的拓扑排序算法,因为节点的遍历顺序就是拓扑排序的结果**。 + +比如刚才举的第一个例子,下图每个节点中的值即入队的顺序: + +![](https://labuladong.online/algo/images/topological-sort/13.jpeg) + +显然,这个顺序就是一个可行的拓扑排序结果。 + +所以,我们稍微修改一下 BFS 版本的环检测算法,记录节点的遍历顺序即可得到拓扑排序的结果: + +```java +class Solution { + + public int[] findOrder(int numCourses, int[][] prerequisites) { + // 建图,和环检测算法相同 + List[] graph = buildGraph(numCourses, prerequisites); + // 计算入度,和环检测算法相同 + int[] indegree = new int[numCourses]; + for (int[] edge : prerequisites) { + int from = edge[1], to = edge[0]; + indegree[to]++; + } + + // 根据入度初始化队列中的节点,和环检测算法相同 + Queue q = new LinkedList<>(); + for (int i = 0; i < numCourses; i++) { + if (indegree[i] == 0) { + q.offer(i); + } + } + + // 记录拓扑排序结果 + int[] res = new int[numCourses]; + // 记录遍历节点的顺序(索引) + int count = 0; + // 开始执行 BFS 算法 + while (!q.isEmpty()) { + int cur = q.poll(); + // 弹出节点的顺序即为拓扑排序结果 + res[count] = cur; + count++; + for (int next : graph[cur]) { + indegree[next]--; + if (indegree[next] == 0) { + q.offer(next); + } + } + } + + if (count != numCourses) { + // 存在环,拓扑排序不存在 + return new int[]{}; + } + + return res; + } + + // 建图函数 + List[] buildGraph(int n, int[][] edges) { + // 见前文 + } +} +``` + +按道理,[图的遍历](https://labuladong.online/algo/data-structure-basic/graph-basic/) 都需要 `visited` 数组防止走回头路,这里的 BFS 算法其实是通过 `indegree` 数组实现的 `visited` 数组的作用,只有入度为 0 的节点才能入队,从而保证不会出现死循环。 + +好了,到这里环检测算法、拓扑排序算法的 BFS 实现也讲完了,继续留一个思考题: + +对于 BFS 的环检测算法,如果问你形成环的节点具体是哪些,你应该如何实现呢? + + + + + + + +
+
+引用本文的文章 + + - [Kruskal 最小生成树算法](https://labuladong.online/algo/data-structure/kruskal/) + - [【强化练习】BFS 经典习题 I](https://labuladong.online/algo/problem-set/bfs/) + - [图结构基础及通用代码实现](https://labuladong.online/algo/data-structure-basic/graph-basic/) + - [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [用算法打败算法](https://labuladong.online/algo/fname.html?fname=PDF中的算法) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [310. Minimum Height Trees](https://leetcode.com/problems/minimum-height-trees/?show=1) | [310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/?show=1) | 🟠 | +| - | [剑指 Offer II 113. 课程顺序](https://leetcode.cn/problems/QA2IGt/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" index bf3020b5bc..c2574b870a 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" @@ -1,27 +1,56 @@ -# 设计Twitter +# 设计朋友圈时间线功能 -「design Twitter」是 LeetCode 上第 335 道题目,不仅题目本身很有意思,而且把合并多个有序链表的算法和面向对象设计(OO design)结合起来了,很有实际意义,本文就带大家来看看这道题。 + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [355. Design Twitter](https://leetcode.com/problems/design-twitter/) | [355. 设计推特](https://leetcode.cn/problems/design-twitter/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [链表基础](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) +> - [哈希表基础](https://labuladong.online/algo/data-structure-basic/hashmap-basic/) +> - [二叉堆基础](https://labuladong.online/algo/data-structure-basic/binary-heap-basic/) + +力扣第 355 「设计推特」不仅题目本身很有意思,而且把合并多个有序链表的算法和面向对象设计(OO design)结合起来了,很有实际意义,本文就带大家来看看这道题。 至于 Twitter 的什么功能跟算法有关系,等我们描述一下题目要求就知道了。 -### 一、题目及应用场景简介 +## 一、题目及应用场景简介 Twitter 和微博功能差不多,我们主要要实现这样几个 API: + + + + ```java class Twitter { - /** user 发表一条 tweet 动态 */ + // user 发表一条 tweet 动态 public void postTweet(int userId, int tweetId) {} - /** 返回该 user 关注的人(包括他自己)最近的动态 id, - 最多 10 条,而且这些动态必须按从新到旧的时间线顺序排列。*/ + // 返回该 user 关注的人(包括他自己)最近的动态 id + // 最多 10 条,而且这些动态必须按从新到旧的时间线顺序排列 public List getNewsFeed(int userId) {} - /** follower 关注 followee,如果 Id 不存在则新建 */ + // follower 关注 followee,如果 Id 不存在则新建 public void follow(int followerId, int followeeId) {} - /** follower 取关 followee,如果 Id 不存在则什么都不做 */ + // follower 取关 followee,如果 Id 不存在则什么都不做 public void unfollow(int followerId, int followeeId) {} } ``` @@ -55,17 +84,23 @@ twitter.getNewsFeed(1); // return [5] ``` + + 这个场景在我们的现实生活中非常常见。拿朋友圈举例,比如我刚加到女神的微信,然后我去刷新一下我的朋友圈动态,那么女神的动态就会出现在我的动态列表,而且会和其他动态按时间排好序。只不过 Twitter 是单向关注,微信好友相当于双向关注。除非,被屏蔽... 这几个 API 中大部分都很好实现,最核心的功能难点应该是 `getNewsFeed`,因为返回的结果必须在时间上有序,但问题是用户的关注是动态变化的,怎么办? -**这里就涉及到算法了**:如果我们把每个用户各自的推文存储在链表里,每个链表节点存储文章 id 和一个时间戳 time(记录发帖时间以便比较),而且这个链表是按 time 有序的,那么如果某个用户关注了 k 个用户,我们就可以用合并 k 个有序链表的算法合并出有序的推文列表,正确地 `getNewsFeed` 了! +**这里就涉及到算法了**:如果我们把每个用户各自的推文存储在链表里,每个链表节点存储文章 `id` 和一个时间戳 `time`(记录发帖时间以便比较),而且这个链表是按 `time` 有序的,那么如果某个用户关注了 `k` 个用户,我们就可以用合并 `k` 个有序链表的算法合并出有序的推文列表,正确地 `getNewsFeed` 了! + +具体的算法等会讲解。不过,就算我们掌握了算法,应该如何编程表示用户 `user` 和推文动态 `tweet` 才能把算法流畅地用出来呢?**这就涉及简单的面向对象设计了**,下面我们来由浅入深,一步一步进行设计。 + +## 二、面向对象设计 + +根据刚才的分析,我们需要一个 `User` 类,储存 `user` 信息,还需要一个 `Tweet` 类,储存推文信息,并且要作为链表的节点。所以我们先搭建一下整体的框架: + -具体的算法等会讲解。不过,就算我们掌握了算法,应该如何编程表示用户 user 和推文动态 tweet 才能把算法流畅地用出来呢?**这就涉及简单的面向对象设计了**,下面我们来由浅入深,一步一步进行设计。 -### 二、面向对象设计 -根据刚才的分析,我们需要一个 User 类,储存 user 信息,还需要一个 Tweet 类,储存推文信息,并且要作为链表的节点。所以我们先搭建一下整体的框架: ```java class Twitter { @@ -73,7 +108,7 @@ class Twitter { private static class Tweet {} private static class User {} - /* 还有那几个 API 方法 */ + // 还有那几个 API 方法 public void postTweet(int userId, int tweetId) {} public List getNewsFeed(int userId) {} public void follow(int followerId, int followeeId) {} @@ -81,12 +116,18 @@ class Twitter { } ``` -之所以要把 Tweet 和 User 类放到 Twitter 类里面,是因为 Tweet 类必须要用到一个全局时间戳 timestamp,而 User 类又需要用到 Tweet 类记录用户发送的推文,所以它们都作为内部类。不过为了清晰和简洁,下文会把每个内部类和 API 方法单独拿出来实现。 -**1、Tweet 类的实现** + +之所以要把 `Tweet` 和 `User` 类放到 `Twitter` 类里面,是因为 `Tweet` 类必须要用到一个全局时间戳 `timestamp`,而 `User` 类又需要用到 `Tweet` 类记录用户发送的推文,所以它们都作为内部类。不过为了清晰和简洁,下文会把每个内部类和 API 方法单独拿出来实现。 + +### Tweet 类的实现 根据前面的分析,Tweet 类很容易实现:每个 Tweet 实例需要记录自己的 tweetId 和发表时间 time,而且作为链表节点,要有一个指向下一个节点的 next 指针。 + + + + ```java class Tweet { private int id; @@ -102,16 +143,22 @@ class Tweet { } ``` -![tweet](../pictures/设计Twitter/tweet.jpg) +![](https://labuladong.online/algo/images/design-twitter/tweet.jpg) -**2、User 类的实现** + + +### User 类的实现 我们根据实际场景想一想,一个用户需要存储的信息有 userId,关注列表,以及该用户发过的推文列表。其中关注列表应该用集合(Hash Set)这种数据结构来存,因为不能重复,而且需要快速查找;推文列表应该由链表这种数据结构储存,以便于进行有序合并的操作。画个图理解一下: -![User](../pictures/设计Twitter/user.jpg) +![](https://labuladong.online/algo/images/design-twitter/user.jpg) 除此之外,根据面向对象的设计原则,「关注」「取关」和「发文」应该是 User 的行为,况且关注列表和推文列表也存储在 User 类中,所以我们也应该给 User 添加 follow,unfollow 和 post 这几个方法: + + + + ```java // static int timestamp = 0 class User { @@ -149,7 +196,7 @@ class User { } ``` -**3、几个 API 方法的实现** +### 几个 API 方法的实现 ```java class Twitter { @@ -160,7 +207,7 @@ class Twitter { // 我们需要一个映射将 userId 和 User 对象对应起来 private HashMap userMap = new HashMap<>(); - /** user 发表一条 tweet 动态 */ + // user 发表一条 tweet 动态 public void postTweet(int userId, int tweetId) { // 若 userId 不存在,则新建 if (!userMap.containsKey(userId)) @@ -169,7 +216,7 @@ class Twitter { u.post(tweetId); } - /** follower 关注 followee */ + // follower 关注 followee public void follow(int followerId, int followeeId) { // 若 follower 不存在,则新建 if(!userMap.containsKey(followerId)){ @@ -184,7 +231,7 @@ class Twitter { userMap.get(followerId).follow(followeeId); } - /** follower 取关 followee,如果 Id 不存在则什么都不做 */ + // follower 取关 followee,如果 Id 不存在则什么都不做 public void unfollow(int followerId, int followeeId) { if (userMap.containsKey(followerId)) { User flwer = userMap.get(followerId); @@ -192,17 +239,23 @@ class Twitter { } } - /** 返回该 user 关注的人(包括他自己)最近的动态 id, - 最多 10 条,而且这些动态必须按从新到旧的时间线顺序排列。*/ + // 返回该 user 关注的人(包括他自己)最近的动态 id + // 最多 10 条,而且这些动态必须按从新到旧的时间线顺序排列 public List getNewsFeed(int userId) { // 需要理解算法,见下文 } } ``` -### 三、算法设计 -实现合并 k 个有序链表的算法需要用到优先级队列(Priority Queue),这种数据结构是「二叉堆」最重要的应用,你可以理解为它可以对插入的元素自动排序。乱序的元素插入其中就被放到了正确的位置,可以按照从小到大(或从大到小)有序地取出元素。 + +## 三、算法设计 + +实现合并 k 个有序链表的算法需要用到优先级队列(Priority Queue),这种数据结构是二叉堆最重要的应用。你可以理解为它可以对插入的元素自动排序,乱序的元素插入其中就被放到了正确的位置,可以按照从小到大(或从大到小)有序地取出元素。具体可以看这篇 [二叉堆实现优先级队列](https://labuladong.online/algo/data-structure-basic/binary-heap-implement/)。 + + + + ```python PriorityQueue pq @@ -216,69 +269,77 @@ while pq not empty: # 输出有序:1,2,4,6,9 ``` -借助这种牛逼的数据结构支持,我们就很容易实现这个核心功能了。注意我们把优先级队列设为按 time 属性**从大到小降序排列**,因为 time 越大意味着时间越近,应该排在前面: + + +借助这种牛逼的数据结构支持,我们就很容易实现这个核心功能了。注意我们把优先级队列设为按 `time` 属性**从大到小降序排列**,因为 `time` 越大意味着时间越近,应该排在前面: + + + + ```java -public List getNewsFeed(int userId) { - List res = new ArrayList<>(); - if (!userMap.containsKey(userId)) return res; - // 关注列表的用户 Id - Set users = userMap.get(userId).followed; - // 自动通过 time 属性从大到小排序,容量为 users 的大小 - PriorityQueue pq = - new PriorityQueue<>(users.size(), (a, b)->(b.time - a.time)); - - // 先将所有链表头节点插入优先级队列 - for (int id : users) { - Tweet twt = userMap.get(id).head; - if (twt == null) continue; - pq.add(twt); - } +class Twitter { + // 为了节约篇幅,省略上文给出的代码部分... + + public List getNewsFeed(int userId) { + List res = new ArrayList<>(); + if (!userMap.containsKey(userId)) return res; + // 关注列表的用户 Id + Set users = userMap.get(userId).followed; + // 自动通过 time 属性从大到小排序,容量为 users 的大小 + PriorityQueue pq = + new PriorityQueue<>(users.size(), (a, b)->(b.time - a.time)); + + // 先将所有链表头节点插入优先级队列 + for (int id : users) { + Tweet twt = userMap.get(id).head; + if (twt == null) continue; + pq.add(twt); + } - while (!pq.isEmpty()) { - // 最多返回 10 条就够了 - if (res.size() == 10) break; - // 弹出 time 值最大的(最近发表的) - Tweet twt = pq.poll(); - res.add(twt.id); - // 将下一篇 Tweet 插入进行排序 - if (twt.next != null) - pq.add(twt.next); + while (!pq.isEmpty()) { + // 最多返回 10 条就够了 + if (res.size() == 10) break; + // 弹出 time 值最大的(最近发表的) + Tweet twt = pq.poll(); + res.add(twt.id); + // 将下一篇 Tweet 插入进行排序 + if (twt.next != null) + pq.add(twt.next); + } + return res; } - return res; } ``` + + 这个过程是这样的,下面是我制作的一个 GIF 图描述合并链表的过程。假设有三个 Tweet 链表按 time 属性降序排列,我们把他们降序合并添加到 res 中。注意图中链表节点中的数字是 time 属性,不是 id 属性: -![gif](../pictures/设计Twitter/merge.gif) +![](https://labuladong.online/algo/images/design-twitter/merge.gif) + +至此,这道一个极其简化的 Twitter 时间线功能就设计完毕了,更多数据结构设计相关的题目参见 [数据结构设计经典习题](https://labuladong.online/algo/problem-set/ds-design/)。 + -至此,这道一个极其简化的 Twitter 时间线功能就设计完毕了。 -### 四、最后总结 -本文运用简单的面向对象技巧和合并 k 个有序链表的算法设计了一套简化的时间线功能,这个功能其实广泛地运用在许多社交应用中。 -我们先合理地设计出 User 和 Tweet 两个类,然后基于这个设计之上运用算法解决了最重要的一个功能。可见实际应用中的算法并不是孤立存在的,需要和其他知识混合运用,才能发挥实际价值。 -当然,实际应用中的社交 App 数据量是巨大的,考虑到数据库的读写性能,我们的设计可能承受不住流量压力,还是有些太简化了。而且实际的应用都是一个极其庞大的工程,比如下图,是 Twitter 这样的社交网站大致的系统结构: +
+
+引用本文的文章 -![design](../pictures/设计Twitter/design.png) + - [【强化练习】优先级队列经典习题](https://labuladong.online/algo/problem-set/binary-heap/) -我们解决的问题应该只能算 Timeline Service 模块的一小部分,功能越多,系统的复杂性可能是指数级增长的。所以说合理的顶层设计十分重要,其作用是远超某一个算法的。 +

-最后,Github 上有一个优秀的开源项目,专门收集了很多大型系统设计的案例和解析,而且有中文版本,上面这个图也出自该项目。对系统设计感兴趣的读者可以点击「阅读原文」查看。 -PS:本文前两张图片和 GIF 是我第一次尝试用平板的绘图软件制作的,花了很多时间,尤其是 GIF 图,需要一帧一帧制作。如果本文内容对你有帮助,点个赞分个享,鼓励一下我呗! -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:特殊数据结构:单调队列](../数据结构系列/单调队列.md) -[下一篇:递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" index 1a20d37ea1..dd0fcb9833 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" @@ -1,65 +1,170 @@ -# 递归反转链表的一部分 +# 单链表的花式反转方法汇总 -反转单链表的迭代实现不是一个困难的事情,但是递归实现就有点难度了,如果再加一点难度,让你仅仅反转单链表中的一部分,你是否能**够递归实现**呢? -本文就来由浅入深,step by step 地解决这个问题。如果你还不会递归地反转单链表也没关系,**本文会从递归反转整个单链表开始拓展**,只要你明白单链表的结构,相信你能够有所收获。 + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) | [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | 🟢 | +| [25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/) | [25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) | 🔴 | +| [92. Reverse Linked List II](https://leetcode.com/problems/reverse-linked-list-ii/) | [92. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | 🟠 | + +**-----------** + + + +反转单链表的迭代解法不是一个困难的事情,但是递归实现就有点难度了。如果再加一点难度,让你仅仅反转单链表中的一部分,你是否能够同时用迭代和递归实现呢?再进一步,如果让你 k 个一组反转链表,阁下又应如何应对? + +本文就来由浅入深,一次性解决这些链表操作的问题。我会同时使用递归和迭代的方式,并结合可视化面板帮助你理解,以此强化你的递归思维以及操作链表指针的能力。 + +## 反转整个单链表 + +在 力扣/LeetCode 中,单链表的通用结构是这样的: ```java // 单链表节点的结构 -public class ListNode { +class ListNode { int val; ListNode next; ListNode(int x) { val = x; } } ``` -什么叫反转单链表的一部分呢,就是给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变: +单链表反转是一个比较基础的算法题,力扣第 206 题「反转链表」就是这个问题: -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/title.png) + -**注意这里的索引是从 1 开始的**。迭代的思路大概是:先用一个 for 循环找到第 `m` 个位置,然后再用一个 for 循环将 `m` 和 `n` 之间的元素反转。但是我们的递归解法不用一个 for 循环,纯递归实现反转。 +下面我们来尝试用多种方法解决这个问题。 -迭代实现思路看起来虽然简单,但是细节问题很多的,反而不容易写对。相反,递归实现就很简洁优美,下面就由浅入深,先从反转整个单链表说起。 +### 迭代解法 -### 一、递归反转整个链表 +这道题的常规做法就是迭代解法,通过操作几个指针,将链表中的每个节点的指针方向反转,没什么难点,主要是指针操作的细节问题。 -这个算法可能很多读者都听说过,这里详细介绍一下,先直接看实现代码: +这里直接给出代码,结合注释和可视化面板应该不难理解: ```java -ListNode reverse(ListNode head) { - if (head.next == null) return head; - ListNode last = reverse(head.next); - head.next.next = head; - head.next = null; - return last; +class Solution { + // 反转以 head 为起点的单链表 + public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + // 由于单链表的结构,至少要用三个指针才能完成迭代反转 + // cur 是当前遍历的节点,pre 是 cur 的前驱结点,nxt 是 cur 的后继结点 + ListNode pre, cur, nxt; + pre = null; cur = head; nxt = head.next; + while (cur != null) { + // 逐个结点反转 + cur.next = pre; + // 更新指针位置 + pre = cur; + cur = nxt; + if (nxt != null) { + nxt = nxt.next; + } + } + // 返回反转后的头结点 + return pre; + } } ``` -看起来是不是感觉不知所云,完全不能理解这样为什么能够反转链表?这就对了,这个算法常常拿来显示递归的巧妙和优美,我们下面来详细解释一下这段代码。 + + +你可以点开下面的可视化面板,多次点击 cur.next = pre 这一行代码,即可直观地看到单链表的反转过程: + + + +> [!TIP] +> 上面操作单链表的代码逻辑不复杂,而且也不止我这一种正确的写法。但是操作指针的时候,有一些很基本、很简单的小技巧,可以让你写代码的思路更清晰: +> +> 1、一旦出现类似 `nxt.next` 这种操作,就要条件反射地想到,先判断 `nxt` 是否为 null,否则容易出现空指针异常。 +> +> 2、注意循环的终止条件。你要知道循环终止时,各个指针的位置,这样才能保返回正确的答案。如果你觉得有点复杂想不清楚,那就动手画一个最简单的场景跑一下算法,比如这道题就可以画一个只有两个节点的单链表 `1->2`,然后就能确定循环终止后各个指针的位置了。 + +### 递归解法 + +上面的迭代解法操作指针虽然有些繁琐,但是思路还是比较清晰的。如果现在让你用递归来反转单链表,你有啥想法没? + +对于初学者来说可能很难想到,这很正常。如果你学习了后文的二叉树系列算法思维,回头再来看这道题,才有可能自己想出这个算法。 + +因为二叉树结构本身就是单链表的延伸,相当于是二叉链表嘛,所以二叉树上的递归思维,套用到单链表上是一样的。 + +**递归反转单链表的关键在于,这个问题本身是存在子问题结构的**。 + +比方说,现在给你输入一个以 `1` 为头结点单链表 `1->2->3->4`,那么如果我忽略这个头结点 `1`,只拿出 `2->3->4` 这个子链表,它也是个单链表对吧? + +那么你这个 `reverseList` 函数,只要输入一个单链表,就能给我反转对吧?那么你能不能用这个函数先来反转 `2->3->4` 这个子链表呢,然后再想办法把 `1` 接到反转后的 `4->3->2` 的最后面,是不是就完成了整个链表的反转? + + + + + +```java +reverseList(1->2->3->4) = reverseList(2->3->4) -> 1 +``` + + + +**这就是「分解问题」的思路,通过递归函数的定义,把原问题分解成若干规模更小、结构相同的子问题,最后通过子问题的答案组装原问题的解**。 + +在后面的教程中会有专门的章节讲解和练习这种思维,这里不展开。 + +先来看看递归反转单链表的代码实现: + +```java +class Solution { + // 定义:输入一个单链表头结点,将该链表反转,返回新的头结点 + public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode last = reverseList(head.next); + head.next.next = head; + head.next = null; + return last; + } +} +``` -**对于递归算法,最重要的就是明确递归函数的定义**。具体来说,我们的 `reverse` 函数定义是这样的: +这个算法常常拿来显示递归的巧妙和优美,我们下面来详细解释一下这段代码,最后在给出可视化面板,你可以自己动手探究一下递归过程。 + +对于「分解问题」思路的递归算法,最重要的就是明确递归函数的定义。具体来说,我们的 `reverseList` 函数定义是这样的: **输入一个节点 `head`,将「以 `head` 为起点」的链表反转,并返回反转之后的头结点**。 -明白了函数的定义,在来看这个问题。比如说我们想反转这个链表: +明白了函数的定义,再来看这个问题。比如说我们想反转这个链表: + +![](https://labuladong.online/algo/images/reverse-linked-list/1.jpg) + +那么输入 `reverseList(head)` 后,会在这里进行递归: + + -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/1.jpg) -那么输入 `reverse(head)` 后,会在这里进行递归: ```java -ListNode last = reverse(head.next); +ListNode last = reverseList(head.next); ``` + + 不要跳进递归(你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果: -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/2.jpg) +![](https://labuladong.online/algo/images/reverse-linked-list/2.jpg) -这个 `reverse(head.next)` 执行完成后,整个链表就成了这样: +这个 `reverseList(head.next)` 执行完成后,整个链表就成了这样: -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/3.jpg) +![](https://labuladong.online/algo/images/reverse-linked-list/3.jpg) -并且根据函数定义,`reverse` 函数会返回反转之后的头结点,我们用变量 `last` 接收了。 +并且根据函数定义,`reverseList` 函数会返回反转之后的头结点,我们用变量 `last` 接收了。 现在再来看下面的代码: @@ -67,7 +172,7 @@ ListNode last = reverse(head.next); head.next.next = head; ``` -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/4.jpg) +![](https://labuladong.online/algo/images/reverse-linked-list/4.jpg) 接下来: @@ -76,17 +181,25 @@ head.next = null; return last; ``` -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/5.jpg) +![](https://labuladong.online/algo/images/reverse-linked-list/5.jpg) + + + + + + 神不神奇,这样整个链表就反转过来了!递归代码就是这么简洁优雅,不过其中有两个地方需要注意: 1、递归函数要有 base case,也就是这句: ```java -if (head.next == null) return head; +if (head == null || head.next == null) { + return head; +} ``` -意思是如果链表只有一个节点的时候反转也是它自己,直接返回即可。 +意思是如果链表为空或者只有一个节点的时候,反转结果就是它自己,直接返回即可。 2、当链表递归反转之后,新的头结点是 `last`,而之前的 `head` 变成了最后一个节点,别忘了链表的末尾要指向 null: @@ -94,9 +207,30 @@ if (head.next == null) return head; head.next = null; ``` -理解了这两点后,我们就可以进一步深入了,接下来的问题其实都是在这个算法上的扩展。 +这样,整个单链表就完成反转了,神不神奇?下面是递归反转链表的可视化过程: + -### 二、反转链表前 N 个节点 +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ +> [!NOTE] +> 虽然可视化面板可以展示整个递归过程的所有细节,但我不建议初学者过于执着于细节。建议先依照上面图示讲解的思维方式理解递归,然后再通过可视化面板加深理解。 + +> [!NOTE] +> 值得一提的是,递归操作链表并不高效。 +> +> 递归解法和迭代解法相比,时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。 +> +> 所以递归操作链表可以用来练习递归思维,但是考虑效率的话还是使用迭代算法更好。 + +## 反转链表前 N 个节点 这次我们实现一个这样的函数: @@ -107,16 +241,65 @@ ListNode reverseN(ListNode head, int n) 比如说对于下图链表,执行 `reverseN(head, 3)`: -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/6.jpg) +![](https://labuladong.online/algo/images/reverse-linked-list/6.jpg) + + -解决思路和反转整个链表差不多,只要稍加修改即可: + + + + +### 迭代解法 + +迭代解法应该比较好写,在之前实现的 `reverseList` 基础上稍加修改就可以了: ```java -ListNode successor = null; // 后驱节点 +ListNode reverseN(ListNode head, int n) { + if (head == null || head.next == null) { + return head; + } + ListNode pre, cur, nxt; + pre = null; cur = head; nxt = head.next; + while (n > 0) { + cur.next = pre; + pre = cur; + cur = nxt; + if (nxt != null) { + nxt = nxt.next; + } + n--; + } + // 此时的 cur 是第 n + 1 个节点,head 是反转后的尾结点 + head.next = cur; + // 此时的 pre 是反转后的头结点 + return pre; +} +``` + + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + +### 递归解法 + +递归思路和递归反转整个链表差不多,只要稍加修改即可: + +```java +// 后驱节点 +ListNode successor = null; // 反转以 head 为起点的 n 个节点,返回新的头结点 ListNode reverseN(ListNode head, int n) { - if (n == 1) { + if (n == 1) { // 记录第 n + 1 个节点 successor = head.next; return head; @@ -128,73 +311,315 @@ ListNode reverseN(ListNode head, int n) { // 让反转之后的 head 节点和后面的节点连起来 head.next = successor; return last; -} +} ``` 具体的区别: -1、base case 变为 `n == 1`,反转一个元素,就是它本身,同时**要记录后驱节点**。 +1、base case 变为 `n == 1`,反转一个元素,就是它本身,**同时要记录后驱节点**,即要记录第 `n + 1` 个节点。 + +2、刚才我们直接把 `head.next` 设置为 null,因为整个链表反转后原来的 `head` 变成了整个链表的最后一个节点。但现在 `head` 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 `successor`(第 `n + 1` 个节点),反转之后将 `head` 连接上。 -2、刚才我们直接把 `head.next` 设置为 null,因为整个链表反转后原来的 `head` 变成了整个链表的最后一个节点。但现在 `head` 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 `successor`(第 n + 1 个节点),反转之后将 `head` 连接上。 -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/7.jpg) -OK,如果这个函数你也能看懂,就离实现「反转一部分链表」不远了。 -### 三、反转链表的一部分 -现在解决我们最开始提出的问题,给一个索引区间 `[m,n]`(索引从 1 开始),仅仅反转区间中的链表元素。 +![](https://labuladong.online/algo/images/reverse-linked-list/7.jpg) + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +## 反转链表的一部分 + +我们可以再进一步,给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变。 + +力扣第 92 题「反转链表 II」就是这个问题: + + + + + + + + + +题目输入索引区间 `[m, n]`(索引从 1 开始),仅仅反转区间中的链表元素,函数签名如下: ```java ListNode reverseBetween(ListNode head, int m, int n) ``` -首先,如果 `m == 1`,就相当于反转链表开头的 `n` 个元素嘛,也就是我们刚才实现的功能: +### 迭代解法 + +纯迭代的思路比较直接,可以先找到第 `m - 1` 个节点,然后复用之前实现的 `reverseN` 函数就行了: ```java -ListNode reverseBetween(ListNode head, int m, int n) { - // base case - if (m == 1) { - // 相当于反转前 n 个元素 - return reverseN(head, n); +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + if (m == 1) { + return reverseN(head, n); + } + // 找到第 m 个节点的前驱 + ListNode pre = head; + for (int i = 1; i < m - 1; i++) { + pre = pre.next; + } + // 从第 m 个节点开始反转 + pre.next = reverseN(pre.next, n - m + 1); + return head; + } + + ListNode reverseN(ListNode head, int n) { + if (head == null || head.next == null) { + return head; + } + ListNode pre, cur, nxt; + pre = null; cur = head; nxt = head.next; + while (n > 0) { + cur.next = pre; + pre = cur; + cur = nxt; + if (nxt != null) { + nxt = nxt.next; + } + n--; + } + // 此时的 cur 是第 n + 1 个节点,head 是反转后的尾结点 + head.next = cur; + // 此时的 pre 是反转后的头结点 + return pre; } - // ... } ``` -如果 `m != 1` 怎么办?如果我们把 `head` 的索引视为 1,那么我们是想从第 `m` 个元素开始反转对吧;如果把 `head.next` 的索引视为 1 呢?那么相对于 `head.next`,反转的区间应该是从第 `m - 1` 个元素开始的;那么对于 `head.next.next` 呢…… -区别于迭代思想,这就是递归思想,所以我们可以完成代码: +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ + + +### 递归解法 + +纯递归解法,依然是找到第 `m - 1` 个节点,然后复用之前实现的 `reverseN` 函数就行了。 + +关键是,如何通过递归的方式找到第 `m - 1` 个节点呢? + +如果我们把 `head` 的索引视为 1,那么我们是想从第 `m` 个元素开始反转对吧;如果把 `head.next` 的索引视为 1 呢?那么相对于 `head.next`,反转的区间应该是从第 `m - 1` 个元素开始的;那么对于 `head.next.next` 呢…… + +这其实就是用递归的方式来进行迭代。我们可以这样写代码: ```java -ListNode reverseBetween(ListNode head, int m, int n) { - // base case - if (m == 1) { - return reverseN(head, n); +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + // base case + if (m == 1) { + return reverseN(head, n); + } + // 前进到反转的起点触发 base case + head.next = reverseBetween(head.next, m - 1, n - 1); + return head; + } + + // 后驱节点 + ListNode successor = null; + + // 反转以 head 为起点的 n 个节点,返回新的头结点 + ListNode reverseN(ListNode head, int n) { + if (n == 1) { + // 记录第 n + 1 个节点 + successor = head.next; + return head; + } + ListNode last = reverseN(head.next, n - 1); + + head.next.next = head; + head.next = successor; + return last; } - // 前进到反转的起点触发 base case - head.next = reverseBetween(head.next, m - 1, n - 1); - return head; } ``` -至此,我们的最终大 BOSS 就被解决了。 -### 四、最后总结 +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +## K 个一组反转链表 + +这个问题经常在面经中看到,而且力扣上难度是 Hard,看下题目: + + + +有了前面的层层铺垫,它真的有那么难吗?其实只要你运用一下「分解问题」的思维,然后直接复用前面的 `reverseN` 函数就行了。 + + + + + + + +### 思路分析 + +认真思考一下可以发现**这个问题具有递归性质**。 + +比如说我们对这个链表调用 `reverseKGroup(head, 2)`,即以 2 个节点为一组反转链表: + +![](https://labuladong.online/algo/images/kgroup/1.jpg) + +如果我设法把前 2 个节点反转,那么后面的那些节点怎么处理?后面的这些节点也是一条链表,而且规模(长度)比原来这条链表小,这就叫规模更小,结构相同的子问题。 + +我们可以把原先的 `head` 指针移动到后面这一段链表的开头,然后继续递归调用 `reverseKGroup(head, 2)`: + +![](https://labuladong.online/algo/images/kgroup/2.jpg) + +发现了递归性质,就可以得到大致的算法流程: + +**1、先反转以 `head` 开头的 `k` 个元素**。这里可以复用前面实现的 `reverseN` 函数。 + +![](https://labuladong.online/algo/images/kgroup/3.jpg) + +**2、将第 `k + 1` 个元素作为 `head` 递归调用 `reverseKGroup` 函数**。 + +![](https://labuladong.online/algo/images/kgroup/4.jpg) + +**3、将上述两个过程的结果连接起来**。 + +![](https://labuladong.online/algo/images/kgroup/5.jpg) + +### 代码实现 + +结合上面的逐步讲解,代码就可以直接写出来了。我这里就用迭代形式的 `reverseN` 函数,你想用递归形式的也可以: + +```java +class Solution { + public ListNode reverseKGroup(ListNode head, int k) { + if (head == null) return null; + // 区间 [a, b) 包含 k 个待反转元素 + ListNode a, b; + a = b = head; + for (int i = 0; i < k; i++) { + // 不足 k 个,不需要反转了 + if (b == null) return head; + b = b.next; + } + // 反转前 k 个元素 + ListNode newHead = reverseN(a, k); + // 此时 b 指向下一组待反转的头结点 + // 递归反转后续链表并连接起来 + a.next = reverseKGroup(b, k); + return newHead; + } + + // 上文实现的反转前 N 个节点的函数 + ListNode reverseN(ListNode head, int n) { + if (head == null || head.next == null) { + return head; + } + ListNode pre, cur, nxt; + pre = null; cur = head; nxt = head.next; + while (n > 0) { + cur.next = pre; + pre = cur; + cur = nxt; + if (nxt != null) { + nxt = nxt.next; + } + n--; + } + head.next = cur; + return pre; + } +} +``` + +很快啊,这道题就解决了。 + + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + +## 最后总结 递归的思想相对迭代思想,稍微有点难以理解,处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。 处理看起来比较困难的问题,可以尝试化整为零,把一些简单的解法进行修改,解决困难的问题。 -值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习或者拿去和小伙伴装逼,但是考虑效率的话还是使用迭代算法更好。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:设计Twitter](../数据结构系列/设计Twitter.md) -[下一篇:队列实现栈\|栈实现队列](../数据结构系列/队列实现栈栈实现队列.md) -[目录](../README.md#目录) \ No newline at end of file +
+
+引用本文的文章 + + - [【强化练习】链表双指针经典习题](https://labuladong.online/algo/problem-set/linkedlist-two-pointers/) + - [二叉树心法(思路篇)](https://labuladong.online/algo/data-structure/binary-tree-part1/) + - [如何判断回文链表](https://labuladong.online/algo/data-structure/palindrome-linked-list/) + - [烧饼排序算法](https://labuladong.online/algo/frequency-interview/pancake-sorting/) + - [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [2. Add Two Numbers](https://leetcode.com/problems/add-two-numbers/?show=1) | [2. 两数相加](https://leetcode.cn/problems/add-two-numbers/?show=1) | 🟠 | +| [24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/?show=1) | [24. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/?show=1) | 🟠 | +| [445. Add Two Numbers II](https://leetcode.com/problems/add-two-numbers-ii/?show=1) | [445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/?show=1) | 🟠 | +| - | [剑指 Offer 24. 反转链表](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/?show=1) | 🟢 | +| - | [剑指 Offer II 024. 反转链表](https://leetcode.cn/problems/UHnkqh/?show=1) | 🟢 | +| - | [剑指 Offer II 025. 链表中的两数相加](https://leetcode.cn/problems/lMSNwu/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" index ee63c2221a..7ef325ba29 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" @@ -1,35 +1,76 @@ -# 队列实现栈|栈实现队列 +# 队列实现栈以及栈实现队列 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [225. Implement Stack using Queues](https://leetcode.com/problems/implement-stack-using-queues/) | [225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/) | 🟢 | +| [232. Implement Queue using Stacks](https://leetcode.com/problems/implement-queue-using-stacks/) | [232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/) | 🟢 | +| - | [剑指 Offer 09. 用两个栈实现队列](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) +> - [链表基础](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) +> - [队列基础](https://labuladong.online/algo/data-structure-basic/queue-stack-basic/) 队列是一种先进先出的数据结构,栈是一种先进后出的数据结构,形象一点就是这样: -![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/1.jpg) +![](https://labuladong.online/algo/images/stack-queue/1.jpg) + +这两种数据结构底层其实都是数组或者链表实现的,只是 API 限定了它们的特性,具体实现可以参见基础知识章节的 [队列/栈的原理及实现](https://labuladong.online/algo/data-structure-basic/queue-stack-basic/)。 -这两种数据结构底层其实都是数组或者链表实现的,只是 API 限定了它们的特性,那么今天就来看看如何使用「栈」的特性来实现一个「队列」,如何用「队列」实现一个「栈」。 +今天来看看如何使用「栈」的特性来实现一个「队列」,如何用「队列」实现一个「栈」。 ### 一、用栈实现队列 -首先,队列的 API 如下: +力扣第 232 题「用栈实现队列」让我们实现的 API 如下: ```java class MyQueue { - /** 添加元素到队尾 */ + // 添加元素到队尾 public void push(int x); - /** 删除队头的元素并返回 */ + // 删除队头的元素并返回 public int pop(); - /** 返回队头元素 */ + // 返回队头元素 public int peek(); - /** 判断队列是否为空 */ + // 判断队列是否为空 public boolean empty(); } ``` 我们使用两个栈 `s1, s2` 就能实现一个队列的功能(这样放置栈可能更容易理解): -![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/2.jpg) +![](https://labuladong.online/algo/images/stack-queue/2.jpg) + +当调用 `push` 让元素入队时,只要把元素压入 `s1` 即可,比如说 `push` 进 3 个元素分别是 1,2,3,那么底层结构就是这样: + +![](https://labuladong.online/algo/images/stack-queue/3.jpg) + +那么如果这时候使用 `peek` 查看队头的元素怎么办呢?按道理队头元素应该是 1,但是在 `s1` 中 1 被压在栈底,现在就要轮到 `s2` 起到一个中转的作用了:当 `s2` 为空时,可以把 `s1` 的所有元素取出再添加进 `s2`,**这时候 `s2` 中元素就是先进先出顺序了**: + +![](https://labuladong.online/algo/images/stack-queue/4.jpg) + +当 `s2` 中存在元素时,直接调用操作 `s2` 的 `pop` 方法,弹出的就是最先插入的元素,即实现了队列的 `pop` 操作。 + +完整代码如下: ```java class MyQueue { @@ -39,81 +80,68 @@ class MyQueue { s1 = new Stack<>(); s2 = new Stack<>(); } - // ... -} -``` -当调用 `push` 让元素入队时,只要把元素压入 `s1` 即可,比如说 `push` 进 3 个元素分别是 1,2,3,那么底层结构就是这样: - -![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/3.jpg) - -```java -/** 添加元素到队尾 */ -public void push(int x) { - s1.push(x); -} -``` - -那么如果这时候使用 `peek` 查看队头的元素怎么办呢?按道理队头元素应该是 1,但是在 `s1` 中 1 被压在栈底,现在就要轮到 `s2` 起到一个中转的作用了:当 `s2` 为空时,可以把 `s1` 的所有元素取出再添加进 `s2`,**这时候 `s2` 中元素就是先进先出顺序了**。 - -![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/4.jpg) - -```java -/** 返回队头元素 */ -public int peek() { - if (s2.isEmpty()) - // 把 s1 元素压入 s2 - while (!s1.isEmpty()) - s2.push(s1.pop()); - return s2.peek(); -} -``` - -同理,对于 `pop` 操作,只要操作 `s2` 就可以了。 - -```java -/** 删除队头的元素并返回 */ -public int pop() { - // 先调用 peek 保证 s2 非空 - peek(); - return s2.pop(); -} -``` + // 添加元素到队尾 + public void push(int x) { + s1.push(x); + } + + // 返回队头元素 + public int peek() { + if (s2.isEmpty()) + // 把 s1 元素压入 s2 + while (!s1.isEmpty()) { + s2.push(s1.pop()); + } + return s2.peek(); + } -最后,如何判断队列是否为空呢?如果两个栈都为空的话,就说明队列为空: + // 删除队头元素并返回 + public int pop() { + // 先调用 peek 保证 s2 非空 + peek(); + return s2.pop(); + } -```java -/** 判断队列是否为空 */ -public boolean empty() { - return s1.isEmpty() && s2.isEmpty(); + // 判断队列是否为空 + // 两个栈都为空才说明队列为空 + public boolean empty() { + return s1.isEmpty() && s2.isEmpty(); + } } ``` 至此,就用栈结构实现了一个队列,核心思想是利用两个栈互相配合。 -值得一提的是,这几个操作的时间复杂度是多少呢?有点意思的是 `peek` 操作,调用它时可能触发 `while` 循环,这样的话时间复杂度是 O(N),但是大部分情况下 `while` 循环不会被触发,时间复杂度是 O(1)。由于 `pop` 操作调用了 `peek`,它的时间复杂度和 `peek` 相同。 +值得一提的是,这几个操作的时间复杂度是多少呢? + +有点意思的是 `peek` 操作,调用它时可能触发 `while` 循环,这样的话时间复杂度是 O(N),但是大部分情况下 `while` 循环不会被触发,时间复杂度是 O(1)。由于 `pop` 操作调用了 `peek`,它的时间复杂度和 `peek` 相同。 像这种情况,可以说它们的**最坏时间复杂度**是 O(N),因为包含 `while` 循环,**可能**需要从 `s1` 往 `s2` 搬移元素。 但是它们的**均摊时间复杂度**是 O(1),这个要这么理解:对于一个元素,最多只可能被搬运一次,也就是说 `peek` 操作平均到每个元素的时间复杂度是 O(1)。 +关于时间复杂度的分析方法,详见 [时空复杂度实用分析方法](https://labuladong.online/algo/essential-technique/complexity-analysis/)。 + ### 二、用队列实现栈 -如果说双栈实现队列比较巧妙,那么用队列实现栈就比较简单粗暴了,只需要一个队列作为底层数据结构。首先看下栈的 API: +如果说双栈实现队列比较巧妙,那么用队列实现栈就比较简单粗暴了,只需要一个队列作为底层数据结构就能实现了。 + +力扣第 225 题「用队列实现栈」让我们实现如下 API: ```java class MyStack { - /** 添加元素到栈顶 */ + // 添加元素到栈顶 public void push(int x); - /** 删除栈顶的元素并返回 */ + // 删除栈顶的元素并返回 public int pop(); - /** 返回栈顶元素 */ + // 返回栈顶元素 public int top(); - /** 判断栈是否为空 */ + // 判断栈是否为空 public boolean empty(); } ``` @@ -125,86 +153,151 @@ class MyStack { Queue q = new LinkedList<>(); int top_elem = 0; - /** 添加元素到栈顶 */ + // 添加元素到栈顶 public void push(int x) { // x 是队列的队尾,是栈的栈顶 q.offer(x); top_elem = x; } - /** 返回栈顶元素 */ + // 返回栈顶元素 public int top() { return top_elem; } + + public boolean empty() { + return q.isEmpty(); + } } ``` -我们的底层数据结构是先进先出的队列,每次 `pop` 只能从队头取元素;但是栈是后进先出,也就是说 `pop` API 要从队尾取元素。 +我们的底层数据结构是先进先出的队列,每次 `pop` 只能从队头取元素;但是栈是后进先出,也就是说 `pop` API 要从队尾取元素: -![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/5.jpg) +![](https://labuladong.online/algo/images/stack-queue/5.jpg) 解决方法简单粗暴,把队列前面的都取出来再加入队尾,让之前的队尾元素排到队头,这样就可以取出了: -![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/6.jpg) +![](https://labuladong.online/algo/images/stack-queue/6.jpg) ```java -/** 删除栈顶的元素并返回 */ -public int pop() { - int size = q.size(); - while (size > 1) { - q.offer(q.poll()); - size--; +class MyStack { + // 为了节约篇幅,省略上文给出的代码部分... + + // 删除栈顶的元素并返回 + public int pop() { + int size = q.size(); + while (size > 1) { + q.offer(q.poll()); + size--; + } + // 之前的队尾元素已经到了队头 + return q.poll(); } - // 之前的队尾元素已经到了队头 - return q.poll(); } ``` -这样实现还有一点小问题就是,原来的队尾元素被提到队头并删除了,但是 `top_elem` 变量没有更新,我们还需要一点小修改: +这样实现还有一点小问题就是,原来的队尾元素被推到队头并删除了,但是 `top_elem` 变量没有更新,我们还需要一点小修改: ```java -/** 删除栈顶的元素并返回 */ -public int pop() { - int size = q.size(); - // 留下队尾 2 个元素 - while (size > 2) { +class MyStack { + // 为了节约篇幅,省略上文给出的代码部分... + + // 删除栈顶的元素并返回 + public int pop() { + int size = q.size(); + // 留下队尾 2 个元素 + while (size > 2) { + q.offer(q.poll()); + size--; + } + // 记录新的队尾元素 + top_elem = q.peek(); q.offer(q.poll()); - size--; + // 删除之前的队尾元素 + return q.poll(); } - // 记录新的队尾元素 - top_elem = q.peek(); - q.offer(q.poll()); - // 删除之前的队尾元素 - return q.poll(); } ``` -最后,API `empty` 就很容易实现了,只要看底层的队列是否为空即可: +这样就实现完了,完整的代码如下: ```java -/** 判断栈是否为空 */ -public boolean empty() { - return q.isEmpty(); +class MyStack { + Queue q = new LinkedList<>(); + int top_elem = 0; + + // 添加元素到栈顶 + public void push(int x) { + q.offer(x); + top_elem = x; + } + + // 删除栈顶的元素并返回 + public int pop() { + int size = q.size(); + while (size > 2) { + q.offer(q.poll()); + size--; + } + top_elem = q.peek(); + q.offer(q.poll()); + return q.poll(); + } + + // 返回栈顶元素 + public int top() { + return top_elem; + } + + // 判断栈是否为空 + public boolean empty() { + return q.isEmpty(); + } } ``` -很明显,用队列实现栈的话,`pop` 操作时间复杂度是 O(N),其他操作都是 O(1)​。​ +很明显,用队列实现栈的话,`pop` 操作时间复杂度是 O(N),其他操作都是 O(1)。 -个人认为,用队列实现栈是没啥亮点的问题,但是**用双栈实现队列是值得学习的**。 +个人认为,用队列实现栈是没啥亮点的问题,但是用双栈实现队列是值得学习的。 -![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/4.jpg) +![](https://labuladong.online/algo/images/stack-queue/4.jpg) 从栈 `s1` 搬运元素到 `s2` 之后,元素在 `s2` 中就变成了队列的先进先出顺序,这个特性有点类似「负负得正」,确实不太容易想到。 -希望本文对你有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md) -[下一篇:算法学习之路](../算法思维系列/算法学习之路.md) -[目录](../README.md#目录) \ No newline at end of file +
+
+引用本文的文章 + + - [【强化练习】栈的经典习题](https://labuladong.online/algo/problem-set/stack/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer 09. 用两个栈实现队列](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/?show=1) | 🟢 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" new file mode 100644 index 0000000000..80054eff73 --- /dev/null +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" @@ -0,0 +1,750 @@ +# BFS 算法解题套路框架 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [752. Open the Lock](https://leetcode.com/problems/open-the-lock/) | [752. 打开转盘锁](https://leetcode.cn/problems/open-the-lock/) | 🟠 | +| [773. Sliding Puzzle](https://leetcode.com/problems/sliding-puzzle/) | [773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) +> - [多叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/n-ary-tree-traverse-basic/) +> - [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) + +我多次强调,DFS/回溯/BFS 这类算法,本质上就是把具体的问题抽象成树结构,然后遍历这棵树进行暴力穷举,所以这些穷举算法的代码本质上就是树的遍历代码。 + +梳理一下这里面的因果关系: + +DFS/回溯算法的本质就是递归遍历一棵穷举树(多叉树),而多叉树的递归遍历又是从二叉树的递归遍历衍生出来的。所以我说 DFS/回溯算法的本质是二叉树的递归遍历。 + +BFS 算法的本质就是遍历一幅图,下面你就会看到了,BFS 的算法框架就是 [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) 中遍历图节点的算法代码。 + +而图的遍历算法其实就是多叉树的遍历算法加了个 `visited` 数组防止死循环;多叉树的遍历算法又是从二叉树遍历算法衍生出来的。所以我说 BFS 算法的本质就是二叉树的层序遍历。 + +为啥 BFS 算法经常用来求解最短路径问题?我在 [二叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) 中用二叉树的最小深度这道例题详细说明过。 + +其实所谓的最短路径,都可以类比成二叉树最小深度这类问题(寻找距离根节点最近的叶子节点),递归遍历必须要遍历整棵树的所有节点才能找到目标节点,而层序遍历不需要遍历所有节点就能搞定,所以层序遍历适合解决这类最短路径问题。 + +这么梳理应该够清楚了吧? + +所以阅读本文前,需要确保你学过前面的 [二叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/)、[多叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/n-ary-tree-traverse-basic/) 和 [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/),先把这几种基本数据结构的遍历算法玩明白,其他的算法都会很容易理解。 + +**本文的重点在于,教会你如何对具体的算法问题进行抽象和转化,然后套用 BFS 算法框架进行求解**。 + +在真实的面试笔试题目中,一般不是直接让你遍历树/图这种标准数据结构,而是给你一个具体的场景题,你需要把具体的场景抽象成一个标准的图/树结构,然后利用 BFS 算法穷举得出答案。 + +比方说给你一个迷宫游戏,请你计算走到出口的最小步数?如果这个迷宫还包含传送门,可以瞬间传送到另一个位置,那么最小步数又是多少? + +再比如说两个单词,要求你通过某些替换,把其中一个变成另一个,每次可以替换/删除/插入一个字符,最少要操作几次? + +再比如说连连看游戏,两个方块消除的条件不仅仅是图案相同,还得保证两个方块之间的最短连线不能多于两个拐点。你玩连连看,点击两个坐标,游戏是如何判断它俩的最短连线有几个拐点的? + +你看上面这些例子,是不是感觉和我们前面学习的树/图结构完全扯不上关系?但实际上只要稍加抽象,它们就是树/图结构的遍历,实在是太简单枯燥了。 + +下面用几道例题来讲解 BFS 的套路框架,以后再也不要觉得这类问题难解决了。 + + + + + + + +## 一、算法框架 + +BFS 的算法框架其实就是 [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) 中给出的 BFS 遍历图结构的代码,共有三种写法。 + +对于实际的 BFS 算法问题,第一种写法最简单,但局限性太大,不常用;第二种写法最常用,中等难度的 BFS 算法题基本都可以用这种写法解决;第三种写法稍微复杂一点,但灵活性最高,可能会在一些难度较大的的 BFS 问题中用到。在下一章的 [BFS 算法习题章节](https://labuladong.online/algo/problem-set/bfs/) 中,会有一些难度更大的题目使用第三种写法,到时候你可以自己尝试。 + +本文的例题都是中等难度,所以本文给出的解法都以第二种写法为准: + +```java +// 从 s 开始 BFS 遍历图的所有节点,且记录遍历的步数 +// 当走到目标节点 target 时,返回步数 +int bfs(int s, int target) { + boolean[] visited = new boolean[graph.size()]; + Queue q = new LinkedList<>(); + q.offer(s); + visited[s] = true; + // 记录从 s 开始走到当前节点的步数 + int step = 0; + while (!q.isEmpty()) { + int sz = q.size(); + for (int i = 0; i < sz; i++) { + int cur = q.poll(); + System.out.println("visit " + cur + " at step " + step); + // 判断是否到达终点 + if (cur == target) { + return step; + } + // 将邻居节点加入队列,向四周扩散搜索 + for (int to : neighborsOf(cur)) { + if (!visited[to]) { + q.offer(to); + visited[to] = true; + } + } + } + step++; + } + // 如果走到这里,说明在图中没有找到目标节点 + return -1; +} +``` + +上面这个代码框架几乎就是从 [图结构的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/graph-traverse-basic/) 中复制过来的,只不过添加了一个 `target` 参数,当第一次走到 `target` 时,直接结束算法并返回走过的步数。 + +下面我们用几个具体的例题来看看如何运用这个框架。 + +## 二、773. 滑动谜题 + +力扣第 773 题「滑动谜题」就是一个可以运用 BFS 框架解决的题目,题目的要求如下: + +给你一个 2x3 的滑动拼图,用一个 2x3 的数组 `board` 表示。拼图中有数字 0~5 六个数,其中**数字 0 就表示那个空着的格子**,你可以移动其中的数字,当 `board` 变为 `[[1,2,3],[4,5,0]]` 时,赢得游戏。 + +请你写一个算法,计算赢得游戏需要的最少移动次数,如果不能赢得游戏,返回 -1。 + +比如说输入的二维数组 `board = [[4,1,2],[5,0,3]]`,算法应该返回 5: + +![](https://labuladong.online/algo/images/sliding_puzzle/5.jpeg) + +如果输入的是 `board = [[1,2,3],[5,4,0]]`,则算法返回 -1,因为这种局面下无论如何都不能赢得游戏。 + +我感觉这题还挺有意思的,小时候玩过类似的拼图游戏,比如华容道: + +![](https://labuladong.online/algo/images/sliding_puzzle/2.jpeg) + +你需要移动这些方块,想办法让曹操从初始位置移动到最下方的出口位置。 + +华容道应该比这道题更难一些,因为力扣的这道题中每个方块的大小可以看作是相同的,而华容道中每个方块的大小还不一样。 + +回到这道题,我们如何把这道题抽象成树/图的结构,从而用 BFS 算法框架来解决呢? + +其实棋盘的初始状态就可以认为是起点: + + + + + +``` +[[2,4,1], + [5,0,3]] +``` + + + +我们最终的目标状态是把棋盘变成这样: + + + + + +``` +[[1,2,3], + [4,5,0]] +``` + + + +那么这就可以认为是终点。 + +现在这个问题不就成为了一个图的问题了吗?题目问的其实就是从起点到终点所需的最短路径是多少嘛。 + +起点的邻居节点是谁?把数字 0 和上下左右的数字进行交换,其实就是起点的四个邻居节点嘛(由于本题中棋盘的大小是 2x3,所以索引边界内的实际邻居节点会小于四个): + +![](https://labuladong.online/algo/images/sliding_puzzle/3.jpeg) + +以此类推,这四个邻居节点还有各自的四个邻居节点,那这不就是一幅图结构吗? + +那么我从起点开始使用 BFS 算法遍历这幅图,第一次到达终点时,走过的步数就是答案。 + +伪码如下: + + + + + +```java +int bfs(int[][] board, int[][] target) { + Queue q = new LinkedList<>(); + HashSet visited = new HashSet<>(); + + // 将起点加入队列 + q.offer(board); + visited.add(board); + + int step = 0; + while (!q.isEmpty()) { + int sz = q.size(); + for (int i = 0; i < sz; i++) { + int[][] cur = q.poll(); + // 判断是否到达终点 + if (cur == target) { + return step; + } + // 将当前节点的邻居节点加入队列 + for (int[][] neighbor : getNeighbors(cur)) { + if (!visited.contains(neighbor)) { + q.offer(neighbor); + visited.add(neighbor); + } + } + } + step++; + } + return -1; +} + +List getNeighbors(int[][] board) { + // 将 board 中的数字 0 和上下左右的数字进行交换,得到 4 个邻居节点 +} +``` + + + +对于这道题,我们抽象出来的图结构也是会包含环的,所以需要一个 `visited` 数组记录已经走过的节点,避免成环导致死循环。 + +比如说我从 `[[2,4,1],[5,0,3]]` 节点开始,数字 0 向右移动得到新节点 `[[2,4,1],[5,3,0]]`,但是这个新节点中的 0 也可以向左移动的,又会回到 `[[2,4,1],[5,0,3]]`,这其实就是成环。我们也需要一个 `visited` 哈希集合来记录已经走过的节点,防止成环导致的死循环。 + +还有一个问题,这道题中 `board` 是一个二维数组,我们在 [哈希表/哈希集合原理](https://labuladong.online/algo/data-structure-basic/hashmap-basic/) 中介绍过,二维数组这种可变数据结构是无法直接加入哈希集合的。 + +所以我们还要再用一点技巧,想办法把二维数组转化成一个不可变类型才能存到哈希集合中。常见的解决方案是把二维数组序列化成一个字符串,这样就可以直接存入哈希集合了。 + +**其中比较有技巧性的点在于,二维数组有「上下左右」的概念,压缩成一维的字符串后后,还怎么把数字 0 和上下左右的数字进行交换**? + +对于这道题,题目说输入的数组大小都是 2 x 3,所以我们可以直接手动写出来这个映射: + +```java +// 记录一维字符串的相邻索引 +int[][] neighbor = new int[][]{ + {1, 3}, + {0, 4, 2}, + {1, 5}, + {0, 4}, + {3, 1, 5}, + {4, 2} +}; +``` + +**这个映射的含义就是,在一维字符串中,索引 `i` 在二维数组中的的相邻索引为 `neighbor[i]`**: + +![](https://labuladong.online/algo/images/sliding_puzzle/4.jpeg) + +:::: details 如果是 `m x n` 的二维数组,怎么办? + +对于一个 `m x n` 的二维数组,手写它的一维索引映射肯定不现实了,需要用代码生成它的一维索引映射。 + +观察上图就能发现,如果二维数组中的某个元素 `e` 在一维数组中的索引为 `i`,那么 `e` 的左右相邻元素在一维数组中的索引就是 `i - 1` 和 `i + 1`,而 `e` 的上下相邻元素在一维数组中的索引就是 `i - n` 和 `i + n`,其中 `n` 为二维数组的列数。 + +这样,对于 `m x n` 的二维数组,我们可以写一个函数来生成它的 `neighbor` 索引映射: + +```java +int[][] generateNeighborMapping(int m, int n) { + int[][] neighbor = new int[m * n][]; + for (int i = 0; i < m * n; i++) { + List neighbors = new ArrayList<>(); + + // 如果不是第一列,有左侧邻居 + if (i % n != 0) neighbors.add(i - 1); + + // 如果不是最后一列,有右侧邻居 + if (i % n != n - 1) neighbors.add(i + 1); + + // 如果不是第一行,有上方邻居 + if (i - n >= 0) neighbors.add(i - n); + + // 如果不是最后一行,有下方邻居 + if (i + n < m * n) neighbors.add(i + n); + + // Java 语言特性,将 List 类型转为 int[] 数组 + neighbor[i] = neighbors.stream().mapToInt(Integer::intValue).toArray(); + } + return neighbor; +} +``` + +:::: + + + +这样,无论数字 0 在哪里,都可以通过这个索引映射得到它的相邻索引进行交换了。下面是完整的代码实现: + +```java +class Solution { + public int slidingPuzzle(int[][] board) { + String target = "123450"; + // 将 2x3 的数组转化成字符串作为 BFS 的起点 + String start = ""; + for (int i = 0; i < board.length; i++) { + for (int j = 0; j < board[0].length; j++) { + start = start + board[i][j]; + } + } + + // ****** BFS 算法框架开始 ****** + Queue q = new LinkedList<>(); + HashSet visited = new HashSet<>(); + // 从起点开始 BFS 搜索 + q.offer(start); + visited.add(start); + + int step = 0; + while (!q.isEmpty()) { + int sz = q.size(); + for (int i = 0; i < sz; i++) { + String cur = q.poll(); + // 判断是否达到目标局面 + if (target.equals(cur)) { + return step; + } + // 将数字 0 和相邻的数字交换位置 + for (String neighborBoard : getNeighbors(cur)) { + // 防止走回头路 + if (!visited.contains(neighborBoard)) { + q.offer(neighborBoard); + visited.add(neighborBoard); + } + } + } + step++; + } + // ****** BFS 算法框架结束 ****** + return -1; + } + + private List getNeighbors(String board) { + // 记录一维字符串的相邻索引 + int[][] mapping = new int[][]{ + {1, 3}, + {0, 4, 2}, + {1, 5}, + {0, 4}, + {3, 1, 5}, + {4, 2} + }; + + int idx = board.indexOf('0'); + List neighbors = new ArrayList<>(); + for (int adj : mapping[idx]) { + String new_board = swap(board.toCharArray(), adj, idx); + neighbors.add(new_board); + } + return neighbors; + } + + private String swap(char[] chars, int i, int j) { + char temp = chars[i]; + chars[i] = chars[j]; + chars[j] = temp; + return new String(chars); + } +} +``` + + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + +这道题就解决了。你会发现 BFS 算法本身的写法都是固定的套路,这道题的难点其实在于将题目转化为 BFS 穷举的模型,然后用合理的方法将多维数组转化成字符串,以便哈希集合记录访问过的节点。 + +下面再看一道实际场景题。 + +## 三、解开密码锁的最少次数 + +来看力扣第 752 题「打开转盘锁」,比较有意思: + + + +函数签名如下: + +```java +int openLock(String[] deadends, String target) +``` + +题目中描述的就是我们生活中常见的那种密码锁,如果没有任何约束,最少的拨动次数很好算。比方说想拨到 `"1234"`,那一个个数字拨动就可以了,最少的拨动次数就是 `1 + 2 + 3 + 4 = 10` 次。 + +但现在的难点就在于,在拨动密码锁的过程中不能出现 `deadends`,这样就有一些难度了。如果遇到了 `deadends`,你该怎么处理,才能使得总的拨动次数最少呢? + +千万不要陷入细节,尝试去想各种具体的情况。要知道算法的本质就是穷举,我们直接从 `"0000"` 开始暴力穷举,把所有可能的拨动情况都穷举出来,难道还怕找不到最少的拨动次数么? + +**第一步,我们不管所有的限制条件,不管 `deadends` 和 `target` 的限制,就思考一个问题:如果让你设计一个算法,穷举所有可能的密码组合,你怎么做**? + +就从 `"0000"` 开始,如果你只转一下锁,有几种可能?总共有 4 个位置,每个位置可以向上转,也可以向下转,也就是可以穷举出 `"1000", "9000", "0100", "0900"...` 共 8 种密码。 + +然后,再以这 8 种密码作为基础,其中每个密码又可以转动一下衍生出 8 种密码,以此类推... + +心里那棵递归树出来没有?应该是一棵八叉树,每个节点都有 8 个子节点,向下衍生。 + +下面这段伪码就描述了上述思路,用层序遍历一棵八叉树: + +```java +// 将 s[j] 向上拨动一次 +String plusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '9') + ch[j] = '0'; + else + ch[j] += 1; + return new String(ch); +} +// 将 s[i] 向下拨动一次 +String minusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '0') + ch[j] = '9'; + else + ch[j] -= 1; + return new String(ch); +} + +// BFS 框架,寻找最少的拨动次数 +void BFS(String target) { + Queue q = new LinkedList<>(); + q.offer("0000"); + + int step = 0; + + while (!q.isEmpty()) { + int sz = q.size(); + // 将当前队列中的所有节点向周围扩散 + for (int i = 0; i < sz; i++) { + String cur = q.poll(); + // 判断是否到达终点 + if (cur.equals(target)) { + return step; + } + + // 一个密码可以衍生出 8 种相邻的密码 + for (String neighbor : getNeighbors(cur)) { + q.offer(neighbor); + } + } + // 在这里增加步数 + step++; + } +} +// 将 s 的每一位向上拨动一次或向下拨动一次,8 种相邻密码 +List getNeighbors(String s) { + List neighbors = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + neighbors.add(plusOne(s, i)); + neighbors.add(minusOne(s, i)); + } + return neighbors; +} +``` + +这个代码已经可以穷举所有可能的密码组合了,但是还有些问题需要解决。 + +1、会走回头路,我们可以从 `"0000"` 拨到 `"1000"`,但是等从队列拿出 `"1000"` 时,还会拨出一个 `"0000"`,这样的话会产生死循环。 + +这个问题很好解决,其实就是成环了嘛,我们用一个 `visited` 集合记录已经穷举过的密码,再次遇到时,不要再加到队列里就行了。 + +2、没有对 `deadends` 进行处理,按道理这些「死亡密码」是不能出现的。 + +这个问题也好处理,额外用一个 `deadends` 集合记录这些死亡密码,凡是遇到这些密码,不要加到队列里就行了。 + +或者还可以更简单一些,直接把 `deadends` 中的死亡密码作为 `visited` 集合的初始元素,这样也可以达到目的。 + +下面是完整的代码实现: + +```java +class Solution { + public int openLock(String[] deadends, String target) { + // 记录需要跳过的死亡密码 + Set deads = new HashSet<>(); + for (String s : deadends) deads.add(s); + if (deads.contains("0000")) return -1; + + // 记录已经穷举过的密码,防止走回头路 + Set visited = new HashSet<>(); + Queue q = new LinkedList<>(); + // 从起点开始启动广度优先搜索 + int step = 0; + q.offer("0000"); + visited.add("0000"); + + while (!q.isEmpty()) { + int sz = q.size(); + // 将当前队列中的所有节点向周围扩散 + for (int i = 0; i < sz; i++) { + String cur = q.poll(); + + // 判断是否到达终点 + if (cur.equals(target)) + return step; + + // 将一个节点的合法相邻节点加入队列 + for (String neighbor : getNeighbors(cur)) { + if (!visited.contains(neighbor) && !deads.contains(neighbor)) { + q.offer(neighbor); + visited.add(neighbor); + } + } + } + // 在这里增加步数 + step++; + } + // 如果穷举完都没找到目标密码,那就是找不到了 + return -1; + } + + // 将 s[j] 向上拨动一次 + String plusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '9') + ch[j] = '0'; + else + ch[j] += 1; + return new String(ch); + } + + // 将 s[i] 向下拨动一次 + String minusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '0') + ch[j] = '9'; + else + ch[j] -= 1; + return new String(ch); + } + + // 将 s 的每一位向上拨动一次或向下拨动一次,8 种相邻密码 + List getNeighbors(String s) { + List neighbors = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + neighbors.add(plusOne(s, i)); + neighbors.add(minusOne(s, i)); + } + return neighbors; + } +} +``` + +## 四、双向 BFS 优化 + +下面再介绍一种 BFS 算法的优化思路:**双向 BFS**,可以提高 BFS 搜索的效率。 + +你把这种技巧当做扩展阅读就行,在一般的面试笔试题中,普通的 BFS 算法已经够用了,如果遇到超时无法通过,或者面试官的追问,可以考虑解法是否需要双向 BFS 优化。 + +双向 BFS 就是从标准的 BFS 算法衍生出来的: + +**传统的 BFS 框架是从起点开始向四周扩散,遇到终点时停止;而双向 BFS 则是从起点和终点同时开始扩散,当两边有交集的时候停止**。 + +为什么这样能够能够提升效率呢? + +就好比有 A 和 B 两个人,传统 BFS 就相当于 A 出发去找 B,而 B 待在原地不动;双向 BFS 则是 A 和 B 一起出发,双向奔赴。那当然第二种情况下 A 和 B 可以更快相遇。 + +![](https://labuladong.online/algo/images/bfs/1.jpeg) + +![](https://labuladong.online/algo/images/bfs/2.jpeg) + +图示中的树形结构,如果终点在最底部,按照传统 BFS 算法的策略,会把整棵树的节点都搜索一遍,最后找到 `target`;而双向 BFS 其实只遍历了半棵树就出现了交集,也就是找到了最短距离。 + +当然从 Big O 表示法分析算法复杂度的话,这两种 BFS 在最坏情况下都可能遍历完所有节点,所以理论时间复杂度都是 $O(N)$,但实际运行中双向 BFS 确实会更快一些。 + +::: info 双向 BFS 的局限性 + +**你必须知道终点在哪里,才能使用双向 BFS 进行优化**。 + +对于 BFS 算法,我们肯定是知道起点的,但是终点具体是什么,我们在一开始可能并不知道。 + +比如上面的密码锁问题和滑动拼图问题,题目都明确给出了终点,都可以用双向 BFS 进行优化。 + +但比如我们在 [二叉树的 DFS/BFS 遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) 中讨论的二叉树最小高度的问题,起点是根节点,终点是距离根节点最近的叶子节点,你在算法开始时并不知道终点具体在哪里,所以就没办法使用双向 BFS 进行优化。 + +::: + +下面我们就以密码锁问题为例,看看如何将普通 BFS 算法优化为双向 BFS 算法,直接看代码吧: + +```java +class Solution { + public int openLock(String[] deadends, String target) { + Set deads = new HashSet<>(); + for (String s : deadends) deads.add(s); + // base case + if (deads.contains("0000")) return -1; + if (target.equals("0000")) return 0; + + // 用集合不用队列,可以快速判断元素是否存在 + Set q1 = new HashSet<>(); + Set q2 = new HashSet<>(); + Set visited = new HashSet<>(); + + int step = 0; + q1.add("0000"); + visited.add("0000"); + q2.add(target); + visited.add(target); + + while (!q1.isEmpty() && !q2.isEmpty()) { + // 在这里增加步数 + step++; + + // 哈希集合在遍历的过程中不能修改,所以用 newQ1 存储邻居节点 + Set newQ1 = new HashSet<>(); + + // 获取 q1 中的所有节点的邻居 + for (String cur : q1) { + // 将一个节点的未遍历相邻节点加入集合 + for (String neighbor : getNeighbors(cur)) { + // 判断是否到达终点 + if (q2.contains(neighbor)) { + return step; + } + if (!visited.contains(neighbor) && !deads.contains(neighbor)) { + newQ1.add(neighbor); + visited.add(neighbor); + } + } + } + // newQ1 存储着 q1 的邻居节点 + q1 = newQ1; + // 因为每次 BFS 都是扩散 q1,所以把元素数量少的集合作为 q1 + if (q1.size() > q2.size()) { + Set temp = q1; + q1 = q2; + q2 = temp; + } + } + return -1; + } + + // 将 s[j] 向上拨动一次 + String plusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '9') + ch[j] = '0'; + else + ch[j] += 1; + return new String(ch); + } + + // 将 s[i] 向下拨动一次 + String minusOne(String s, int j) { + char[] ch = s.toCharArray(); + if (ch[j] == '0') + ch[j] = '9'; + else + ch[j] -= 1; + return new String(ch); + } + + List getNeighbors(String s) { + List neighbors = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + neighbors.add(plusOne(s, i)); + neighbors.add(minusOne(s, i)); + } + return neighbors; + } +} +``` + +双向 BFS 还是遵循 BFS 算法框架的,但是有几个细节区别: + +1、不再使用队列存储元素,而是改用 [哈希集合](https://labuladong.online/algo/data-structure-basic/hash-set/),方便快速判两个集合是否有交集。 + +2、调整了 return step 的位置。因为双向 BFS 中不再是简单地判断是否到达终点,而是判断两个集合是否有交集,所以要在计算出邻居节点时就进行判断。 + +3、还有一个优化点,每次都保持 `q1` 是元素数量较小的集合,这样可以一定程度减少搜索次数。 + +因为按照 BFS 的逻辑,队列(集合)中的元素越多,扩散邻居节点之后新的队列(集合)中的元素就越多;在双向 BFS 算法中,如果我们每次都选择一个较小的集合进行扩散,那么占用的空间增长速度就会慢一些,效率就会高一些。 + +不过话说回来,**无论传统 BFS 还是双向 BFS,无论做不做优化,从 Big O 衡量标准来看,时间复杂度都是一样的**,只能说双向 BFS 是一种进阶技巧,算法运行的速度会相对快一点,掌握不掌握其实都无所谓。 + +最关键的还是要把 BFS 通用框架记下来,并且做到熟练运用,后面有 [BFS 习题章节](https://labuladong.online/algo/problem-set/bfs/),请你尝试运用本文的技巧,解决其中的题目。 + + + + + + + +
+
+引用本文的文章 + + - [Prim 最小生成树算法](https://labuladong.online/algo/data-structure/prim/) + - [【强化练习】BFS 经典习题 I](https://labuladong.online/algo/problem-set/bfs/) + - [【强化练习】BFS 经典习题 II](https://labuladong.online/algo/problem-set/bfs-ii/) + - [【强化练习】回溯算法经典习题 II](https://labuladong.online/algo/problem-set/backtrack-ii/) + - [【强化练习】并查集经典习题](https://labuladong.online/algo/problem-set/union-find/) + - [【强化练习】运用层序遍历解题 I](https://labuladong.online/algo/problem-set/binary-tree-level-i/) + - [【强化练习】运用层序遍历解题 II](https://labuladong.online/algo/problem-set/binary-tree-level-ii/) + - [二分图判定算法](https://labuladong.online/algo/data-structure/bipartite-graph/) + - [二叉树基础及常见类型](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) + - [二叉树的递归/层序遍历](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) + - [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [旅游省钱大法:加权最短路径](https://labuladong.online/algo/dynamic-programming/cheap-travel/) + - [环检测及拓扑排序算法](https://labuladong.online/algo/data-structure/topological-sort/) + - [用算法打败算法](https://labuladong.online/algo/fname.html?fname=PDF中的算法) + - [算法学习和心流体验](https://labuladong.online/algo/fname.html?fname=心流) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1091. Shortest Path in Binary Matrix](https://leetcode.com/problems/shortest-path-in-binary-matrix/?show=1) | [1091. 二进制矩阵中的最短路径](https://leetcode.cn/problems/shortest-path-in-binary-matrix/?show=1) | 🟠 | +| [111. Minimum Depth of Binary Tree](https://leetcode.com/problems/minimum-depth-of-binary-tree/?show=1) | [111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/?show=1) | 🟢 | +| [117. Populating Next Right Pointers in Each Node II](https://leetcode.com/problems/populating-next-right-pointers-in-each-node-ii/?show=1) | [117. 填充每个节点的下一个右侧节点指针 II](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/?show=1) | 🟠 | +| [127. Word Ladder](https://leetcode.com/problems/word-ladder/?show=1) | [127. 单词接龙](https://leetcode.cn/problems/word-ladder/?show=1) | 🔴 | +| [1926. Nearest Exit from Entrance in Maze](https://leetcode.com/problems/nearest-exit-from-entrance-in-maze/?show=1) | [1926. 迷宫中离入口最近的出口](https://leetcode.cn/problems/nearest-exit-from-entrance-in-maze/?show=1) | 🟠 | +| [2850. Minimum Moves to Spread Stones Over Grid](https://leetcode.com/problems/minimum-moves-to-spread-stones-over-grid/?show=1) | [2850. 将石头分散到网格图的最少移动次数](https://leetcode.cn/problems/minimum-moves-to-spread-stones-over-grid/?show=1) | 🟠 | +| [286. Walls and Gates](https://leetcode.com/problems/walls-and-gates/?show=1)🔒 | [286. 墙与门](https://leetcode.cn/problems/walls-and-gates/?show=1)🔒 | 🟠 | +| [310. Minimum Height Trees](https://leetcode.com/problems/minimum-height-trees/?show=1) | [310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/?show=1) | 🟠 | +| [329. Longest Increasing Path in a Matrix](https://leetcode.com/problems/longest-increasing-path-in-a-matrix/?show=1) | [329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/?show=1) | 🔴 | +| [365. Water and Jug Problem](https://leetcode.com/problems/water-and-jug-problem/?show=1) | [365. 水壶问题](https://leetcode.cn/problems/water-and-jug-problem/?show=1) | 🟠 | +| [431. Encode N-ary Tree to Binary Tree](https://leetcode.com/problems/encode-n-ary-tree-to-binary-tree/?show=1)🔒 | [431. 将 N 叉树编码为二叉树](https://leetcode.cn/problems/encode-n-ary-tree-to-binary-tree/?show=1)🔒 | 🔴 | +| [433. Minimum Genetic Mutation](https://leetcode.com/problems/minimum-genetic-mutation/?show=1) | [433. 最小基因变化](https://leetcode.cn/problems/minimum-genetic-mutation/?show=1) | 🟠 | +| [490. The Maze](https://leetcode.com/problems/the-maze/?show=1)🔒 | [490. 迷宫](https://leetcode.cn/problems/the-maze/?show=1)🔒 | 🟠 | +| [505. The Maze II](https://leetcode.com/problems/the-maze-ii/?show=1)🔒 | [505. 迷宫 II](https://leetcode.cn/problems/the-maze-ii/?show=1)🔒 | 🟠 | +| [542. 01 Matrix](https://leetcode.com/problems/01-matrix/?show=1) | [542. 01 矩阵](https://leetcode.cn/problems/01-matrix/?show=1) | 🟠 | +| [547. Number of Provinces](https://leetcode.com/problems/number-of-provinces/?show=1) | [547. 省份数量](https://leetcode.cn/problems/number-of-provinces/?show=1) | 🟠 | +| [863. All Nodes Distance K in Binary Tree](https://leetcode.com/problems/all-nodes-distance-k-in-binary-tree/?show=1) | [863. 二叉树中所有距离为 K 的结点](https://leetcode.cn/problems/all-nodes-distance-k-in-binary-tree/?show=1) | 🟠 | +| [994. Rotting Oranges](https://leetcode.com/problems/rotting-oranges/?show=1) | [994. 腐烂的橘子](https://leetcode.cn/problems/rotting-oranges/?show=1) | 🟠 | +| - | [剑指 Offer II 109. 开密码锁](https://leetcode.cn/problems/zlDJc7/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" new file mode 100644 index 0000000000..f1601a6e1e --- /dev/null +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" @@ -0,0 +1,230 @@ +# BFS 算法秒杀各种益智游戏 + +

+GitHub + + + +

+ +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [773. Sliding Puzzle](https://leetcode.com/problems/sliding-puzzle/) | [773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/) | 🔴 + +**-----------** + +滑动拼图游戏大家应该都玩过,下图是一个 4x4 的滑动拼图: + +![](https://labuladong.online/algo/images/sliding_puzzle/1.jpeg) + +拼图中有一个格子是空的,可以利用这个空着的格子移动其他数字。你需要通过移动这些数字,得到某个特定排列顺序,这样就算赢了。 + +我小时候还玩过一款叫做「华容道」的益智游戏,也和滑动拼图比较类似: + +![](https://labuladong.online/algo/images/sliding_puzzle/2.jpeg) + +实际上,滑动拼图游戏也叫数字华容道,你看它俩挺相似的。 + +那么这种游戏怎么玩呢?我记得是有一些套路的,类似于魔方还原公式。但是我们今天不来研究让人头秃的技巧,**这些益智游戏通通可以用暴力搜索算法解决,所以今天我们就学以致用,用 BFS 算法框架来秒杀这些游戏**。 + +### 一、题目解析 + +力扣第 773 题「滑动谜题」就是这个问题,题目的要求如下: + +给你一个 2x3 的滑动拼图,用一个 2x3 的数组 `board` 表示。拼图中有数字 0~5 六个数,其中**数字 0 就表示那个空着的格子**,你可以移动其中的数字,当 `board` 变为 `[[1,2,3],[4,5,0]]` 时,赢得游戏。 + +请你写一个算法,计算赢得游戏需要的最少移动次数,如果不能赢得游戏,返回 -1。 + +比如说输入的二维数组 `board = [[4,1,2],[5,0,3]]`,算法应该返回 5: + +![](https://labuladong.online/algo/images/sliding_puzzle/5.jpeg) + +如果输入的是 `board = [[1,2,3],[5,4,0]]`,则算法返回 -1,因为这种局面下无论如何都不能赢得游戏。 + +### 二、思路分析 + +对于这种计算最小步数的问题,我们就要敏感地想到 BFS 算法。 + +这个题目转化成 BFS 问题是有一些技巧的,我们面临如下问题: + +1、一般的 BFS 算法,是从一个起点 `start` 开始,向终点 `target` 进行寻路,但是拼图问题不是在寻路,而是在不断交换数字,这应该怎么转化成 BFS 算法问题呢? + +2、即便这个问题能够转化成 BFS 问题,如何处理起点 `start` 和终点 `target`?它们都是数组哎,把数组放进队列,套 BFS 框架,想想就比较麻烦且低效。 + +首先回答第一个问题,**BFS 算法并不只是一个寻路算法,而是一种暴力搜索算法**,只要涉及暴力穷举的问题,BFS 就可以用,而且可以最快地找到答案。 + +你想想计算机怎么解决问题的?哪有什么特殊技巧,本质上就是把所有可行解暴力穷举出来,然后从中找到一个最优解罢了。 + +明白了这个道理,我们的问题就转化成了:**如何穷举出 `board` 当前局面下可能衍生出的所有局面**?这就简单了,看数字 0 的位置呗,和上下左右的数字进行交换就行了: + +![](https://labuladong.online/algo/images/sliding_puzzle/3.jpeg) + +这样其实就是一个 BFS 问题,每次先找到数字 0,然后和周围的数字进行交换,形成新的局面加入队列…… 当第一次到达 `target` 时,就得到了赢得游戏的最少步数。 + +对于第二个问题,我们这里的 `board` 仅仅是 2x3 的二维数组,所以可以压缩成一个一维字符串。**其中比较有技巧性的点在于,二维数组有「上下左右」的概念,压缩成一维后,如何得到某一个索引上下左右的索引**? + +对于这道题,题目说输入的数组大小都是 2 x 3,所以我们可以直接手动写出来这个映射: + + +```java +// 记录一维字符串的相邻索引 +int[][] neighbor = new int[][]{ + {1, 3}, + {0, 4, 2}, + {1, 5}, + {0, 4}, + {3, 1, 5}, + {4, 2} +}; +``` + +**这个含义就是,在一维字符串中,索引 `i` 在二维数组中的的相邻索引为 `neighbor[i]`**: + +![](https://labuladong.online/algo/images/sliding_puzzle/4.jpeg) + +那么对于一个 `m x n` 的二维数组,手写它的一维索引映射肯定不现实了,如何用代码生成它的一维索引映射呢? + +观察上图就能发现,如果二维数组中的某个元素 `e` 在一维数组中的索引为 `i`,那么 `e` 的左右相邻元素在一维数组中的索引就是 `i - 1` 和 `i + 1`,而 `e` 的上下相邻元素在一维数组中的索引就是 `i - n` 和 `i + n`,其中 `n` 为二维数组的列数。 + +这样,对于 `m x n` 的二维数组,我们可以写一个函数来生成它的 `neighbor` 索引映射: + +```java +int[][] generateNeighborMapping(int m, int n) { + int[][] neighbor = new int[m * n][]; + for (int i = 0; i < m * n; i++) { + List neighbors = new ArrayList<>(); + + // 如果不是第一列,有左侧邻居 + if (i % n != 0) neighbors.add(i - 1); + // 如果不是最后一列,有右侧邻居 + if (i % n != n - 1) neighbors.add(i + 1); + // 如果不是第一行,有上方邻居 + if (i - n >= 0) neighbors.add(i - n); + // 如果不是最后一行,有下方邻居 + if (i + n < m * n) neighbors.add(i + n); + + // Java 语言特性,将 List 类型转为 int[] 数组 + neighbor[i] = neighbors.stream().mapToInt(Integer::intValue).toArray(); + } + return neighbor; +} +``` + +至此,我们就把这个问题完全转化成标准的 BFS 问题了,借助前文 [BFS 算法框架](https://labuladong.online/algo/essential-technique/bfs-framework/) 的代码框架,直接就可以套出解法代码了: + + +```java +class Solution { + public int slidingPuzzle(int[][] board) { + int m = 2, n = 3; + StringBuilder sb = new StringBuilder(); + String target = "123450"; + // 将 2x3 的数组转化成字符串作为 BFS 的起点 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + sb.append(board[i][j]); + } + } + String start = sb.toString(); + + // 记录一维字符串的相邻索引 + int[][] neighbor = new int[][]{ + {1, 3}, + {0, 4, 2}, + {1, 5}, + {0, 4}, + {3, 1, 5}, + {4, 2} + }; + + /******* BFS 算法框架开始 *******/ + Queue q = new LinkedList<>(); + HashSet visited = new HashSet<>(); + // 从起点开始 BFS 搜索 + q.offer(start); + visited.add(start); + + int step = 0; + while (!q.isEmpty()) { + int sz = q.size(); + for (int i = 0; i < sz; i++) { + String cur = q.poll(); + // 判断是否达到目标局面 + if (target.equals(cur)) { + return step; + } + // 找到数字 0 的索引 + int idx = 0; + for (; cur.charAt(idx) != '0'; idx++) ; + // 将数字 0 和相邻的数字交换位置 + for (int adj : neighbor[idx]) { + String new_board = swap(cur.toCharArray(), adj, idx); + // 防止走回头路 + if (!visited.contains(new_board)) { + q.offer(new_board); + visited.add(new_board); + } + } + } + step++; + } + /******* BFS 算法框架结束 *******/ + return -1; + } + + private String swap(char[] chars, int i, int j) { + char temp = chars[i]; + chars[i] = chars[j]; + chars[j] = temp; + return new String(chars); + } +} +``` + + + +至此,这道题目就解决了,其实框架完全没有变,套路都是一样的,我们只是花了比较多的时间将滑动拼图游戏转化成 BFS 算法。 + +很多益智游戏都是这样,虽然看起来特别巧妙,但都架不住暴力穷举,常用的算法就是回溯算法或者 BFS 算法。 + + + +
+
+引用本文的文章 + + - [BFS 算法解题套路框架](https://labuladong.online/algo/essential-technique/bfs-framework/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | +| :----: | :----: | +| [365. Water and Jug Problem](https://leetcode.com/problems/water-and-jug-problem/?show=1) | [365. 水壶问题](https://leetcode.cn/problems/water-and-jug-problem/?show=1) | + +
+
+ + + +**_____________** + +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" deleted file mode 100644 index 1d737cf70a..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" +++ /dev/null @@ -1,228 +0,0 @@ -# FloodFill算法详解及应用 - -啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色。 - -![floodfill](../pictures/floodfill/floodfill.gif) - -这种算法思想还在许多其他地方有应用。比如说扫雷游戏,有时候你点一个方格,会一下子展开一片区域,这个展开过程,就是 FloodFill 算法实现的。 - -![扫雷](../pictures/floodfill/扫雷.png) - -类似的,像消消乐这类游戏,相同方块积累到一定数量,就全部消除,也是 FloodFill 算法的功劳。 - -![xiaoxiaole](../pictures/floodfill/xiaoxiaole.jpg) - -通过以上的几个例子,你应该对 FloodFill 算法有个概念了,现在我们要抽象问题,提取共同点。 - -### 一、构建框架 - -以上几个例子,都可以抽象成一个二维矩阵(图片其实就是像素点矩阵),然后从某个点开始向四周扩展,直到无法再扩展为止。 - -矩阵,可以抽象为一幅「图」,这就是一个图的遍历问题,也就类似一个 N 叉树遍历的问题。几行代码就能解决,直接上框架吧: - -```java -// (x, y) 为坐标位置 -void fill(int x, int y) { - fill(x - 1, y); // 上 - fill(x + 1, y); // 下 - fill(x, y - 1); // 左 - fill(x, y + 1); // 右 -} -``` - -这个框架可以解决所有在二维矩阵中遍历的问题,说得高端一点,这就叫深度优先搜索(Depth First Search,简称 DFS),说得简单一点,这就叫四叉树遍历框架。坐标 (x, y) 就是 root,四个方向就是 root 的四个子节点。 - -下面看一道 LeetCode 题目,其实就是让我们来实现一个「颜色填充」功能。 - -![title](../pictures/floodfill/leetcode.png) - -根据上篇文章,我们讲了「树」算法设计的一个总路线,今天就可以用到: - -```java -int[][] floodFill(int[][] image, - int sr, int sc, int newColor) { - - int origColor = image[sr][sc]; - fill(image, sr, sc, origColor, newColor); - return image; -} - -void fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出边界索引 - if (!inArea(image, x, y)) return; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return; - image[x][y] = newColor; - - fill(image, x, y + 1, origColor, newColor); - fill(image, x, y - 1, origColor, newColor); - fill(image, x - 1, y, origColor, newColor); - fill(image, x + 1, y, origColor, newColor); -} - -boolean inArea(int[][] image, int x, int y) { - return x >= 0 && x < image.length - && y >= 0 && y < image[0].length; -} -``` - -只要你能够理解这段代码,一定要给你鼓掌,给你 99 分,因为你对「框架思维」的掌控已经炉火纯青,此算法已经 cover 了 99% 的情况,仅有一个细节问题没有解决,就是当 origColor 和 newColor 相同时,会陷入无限递归。 - -### 二、研究细节 - -为什么会陷入无限递归呢,很好理解,因为每个坐标都要搜索上下左右,那么对于一个坐标,一定会被上下左右的坐标搜索。**被重复搜索时,必须保证递归函数能够能正确地退出,否则就会陷入死循环。** - -为什么 newColor 和 origColor 不同时可以正常退出呢?把算法流程画个图理解一下: - -![ppt1](../pictures/floodfill/ppt1.PNG) - -可以看到,fill(1, 1) 被重复搜索了,我们用 fill(1, 1)* 表示这次重复搜索。fill(1, 1)* 执行时,(1, 1) 已经被换成了 newColor,所以 fill(1, 1)* 会在这个 if 语句被怼回去,正确退出了。 - -```java -// 碰壁:遇到其他颜色,超出 origColor 区域 -if (image[x][y] != origColor) return; -``` -![ppt2](../pictures/floodfill/ppt2.PNG) - -但是,如果说 origColor 和 newColor 一样,这个 if 语句就无法让 fill(1, 1)* 正确退出,而是开启了下面的重复递归,形成了死循环。 - -![ppt3](../pictures/floodfill/ppt3.PNG) - -### 三、处理细节 - -如何避免上述问题的发生,最容易想到的就是用一个和 image 一样大小的二维 bool 数组记录走过的地方,一旦发现重复立即 return。 - -```java - // 出界:超出边界索引 -if (!inArea(image, x, y)) return; -// 碰壁:遇到其他颜色,超出 origColor 区域 -if (image[x][y] != origColor) return; -// 不走回头路 -if (visited[x][y]) return; -visited[x][y] = true; -image[x][y] = newColor; -``` - -完全 OK,这也是处理「图」的一种常用手段。不过对于此题,不用开数组,我们有一种更好的方法,那就是回溯算法。 - -前文「回溯算法详解」讲过,这里不再赘述,直接套回溯算法框架: - -```java -void fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出数组边界 - if (!inArea(image, x, y)) return; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return; - // 已探索过的 origColor 区域 - if (image[x][y] == -1) return; - - // choose:打标记,以免重复 - image[x][y] = -1; - fill(image, x, y + 1, origColor, newColor); - fill(image, x, y - 1, origColor, newColor); - fill(image, x - 1, y, origColor, newColor); - fill(image, x + 1, y, origColor, newColor); - // unchoose:将标记替换为 newColor - image[x][y] = newColor; -} -``` - -这种解决方法是最常用的,相当于使用一个特殊值 -1 代替 visited 数组的作用,达到不走回头路的效果。为什么是 -1,因为题目中说了颜色取值在 0 - 65535 之间,所以 -1 足够特殊,能和颜色区分开。 - - -### 四、拓展延伸:自动魔棒工具和扫雷 - -大部分图片编辑软件一定有「自动魔棒工具」这个功能:点击一个地方,帮你自动选中相近颜色的部分。如下图,我想选中老鹰,可以先用自动魔棒选中蓝天背景,然后反向选择,就选中了老鹰。我们来分析一下自动魔棒工具的原理。 - -![抠图](../pictures/floodfill/抠图.jpg) - -显然,这个算法肯定是基于 FloodFill 算法的,但有两点不同:首先,背景色是蓝色,但不能保证都是相同的蓝色,毕竟是像素点,可能存在肉眼无法分辨的深浅差异,而我们希望能够忽略这种细微差异。第二,FloodFill 算法是「区域填充」,这里更像「边界填充」。 - -对于第一个问题,很好解决,可以设置一个阈值 threshold,在阈值范围内波动的颜色都视为 origColor: - -```java -if (Math.abs(image[x][y] - origColor) > threshold) - return; -``` - -对于第二个问题,我们首先明确问题:不要把区域内所有 origColor 的都染色,而是只给区域最外圈染色。然后,我们分析,如何才能仅给外围染色,即如何才能找到最外围坐标,最外围坐标有什么特点? - -![ppt4](../pictures/floodfill/ppt4.PNG) - -可以发现,区域边界上的坐标,至少有一个方向不是 origColor,而区域内部的坐标,四面都是 origColor,这就是解决问题的关键。保持框架不变,使用 visited 数组记录已搜索坐标,主要代码如下: - -```java -int fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出数组边界 - if (!inArea(image, x, y)) return 0; - // 已探索过的 origColor 区域 - if (visited[x][y]) return 1; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return 0; - - visited[x][y] = true; - - int surround = - fill(image, x - 1, y, origColor, newColor) - + fill(image, x + 1, y, origColor, newColor) - + fill(image, x, y - 1, origColor, newColor) - + fill(image, x, y + 1, origColor, newColor); - - if (surround < 4) - image[x][y] = newColor; - - return 1; -} -``` - -这样,区域内部的坐标探索四周后得到的 surround 是 4,而边界的坐标会遇到其他颜色,或超出边界索引,surround 会小于 4。如果你对这句话不理解,我们把逻辑框架抽象出来看: - -```java -int fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出数组边界 - if (!inArea(image, x, y)) return 0; - // 已探索过的 origColor 区域 - if (visited[x][y]) return 1; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return 0; - // 未探索且属于 origColor 区域 - if (image[x][y] == origColor) { - // ... - return 1; - } -} -``` - -这 4 个 if 判断涵盖了 (x, y) 的所有可能情况,surround 的值由四个递归函数相加得到,而每个递归函数的返回值就这四种情况的一种。借助这个逻辑框架,你一定能理解上面那句话了。 - -这样就实现了仅对 origColor 区域边界坐标染色的目的,等同于完成了魔棒工具选定区域边界的功能。 - -这个算法有两个细节问题,一是必须借助 visited 来记录已探索的坐标,而无法使用回溯算法;二是开头几个 if 顺序不可打乱。读者可以思考一下原因。 - -同理,思考扫雷游戏,应用 FloodFill 算法展开空白区域的同时,也需要计算并显示边界上雷的个数,如何实现的?其实也是相同的思路,遇到雷就返回 true,这样 surround 变量存储的就是雷的个数。当然,扫雷的 FloodFill 算法不能只检查上下左右,还得加上四个斜向。 - -![](../pictures/floodfill/ppt5.PNG) - -以上详细讲解了 FloodFill 算法的框架设计,**二维矩阵中的搜索问题,都逃不出这个算法框架**。 - - - - - - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:字符串乘法](../算法思维系列/字符串乘法.md) - -[下一篇:区间调度之区间合并问题](../算法思维系列/区间调度问题之区间合并.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" index 02fde7ad2c..d1a6268bc7 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" @@ -2,6 +2,7 @@ 本章包含一些常用的算法技巧,比如前缀和、回溯思想、位操作、双指针、如何正确书写二分查找等等。 -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: +![labuladong二维码](../pictures/table_qr2.jpg) ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" deleted file mode 100644 index 7f250a12e7..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" +++ /dev/null @@ -1,230 +0,0 @@ -# Union-Find算法应用 - -上篇文章很多读者对于 Union-Find 算法的应用表示很感兴趣,这篇文章就拿几道 LeetCode 题目来讲讲这个算法的巧妙用法。 - -首先,复习一下,Union-Find 算法解决的是图的动态连通性问题,这个算法本身不难,能不能应用出来主要是看你抽象问题的能力,是否能够把原始问题抽象成一个有关图论的问题。 - -先复习一下上篇文章写的算法代码,回答读者提出的几个问题: - -```java -class UF { - // 记录连通分量个数 - private int count; - // 存储若干棵树 - private int[] parent; - // 记录树的“重量” - private int[] size; - - public UF(int n) { - this.count = n; - parent = new int[n]; - size = new int[n]; - for (int i = 0; i < n; i++) { - parent[i] = i; - size[i] = 1; - } - } - - /* 将 p 和 q 连通 */ - public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - - // 小树接到大树下面,较平衡 - if (size[rootP] > size[rootQ]) { - parent[rootQ] = rootP; - size[rootP] += size[rootQ]; - } else { - parent[rootP] = rootQ; - size[rootQ] += size[rootP]; - } - count--; - } - - /* 判断 p 和 q 是否互相连通 */ - public boolean connected(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - // 处于同一棵树上的节点,相互连通 - return rootP == rootQ; - } - - /* 返回节点 x 的根节点 */ - private int find(int x) { - while (parent[x] != x) { - // 进行路径压缩 - parent[x] = parent[parent[x]]; - x = parent[x]; - } - return x; - } - - public int count() { - return count; - } -} -``` - -算法的关键点有 3 个: - -1、用 `parent` 数组记录每个节点的父节点,相当于指向父节点的指针,所以 `parent` 数组内实际存储着一个森林(若干棵多叉树)。 - -2、用 `size` 数组记录着每棵树的重量,目的是让 `union` 后树依然拥有平衡性,而不会退化成链表,影响操作效率。 - -3、在 `find` 函数中进行路径压缩,保证任意树的高度保持在常数,使得 `union` 和 `connected` API 时间复杂度为 O(1)。 - -有的读者问,**既然有了路径压缩,`size` 数组的重量平衡还需要吗**?这个问题很有意思,因为路径压缩保证了树高为常数(不超过 3),那么树就算不平衡,高度也是常数,基本没什么影响。 - -我认为,论时间复杂度的话,确实,不需要重量平衡也是 O(1)。但是如果加上 `size` 数组辅助,效率还是略微高一些,比如下面这种情况: - -![](../pictures/unionfind应用/1.jpg) - -如果带有重量平衡优化,一定会得到情况一,而不带重量优化,可能出现情况二。高度为 3 时才会触发路径压缩那个 `while` 循环,所以情况一根本不会触发路径压缩,而情况二会多执行很多次路径压缩,将第三层节点压缩到第二层。 - -也就是说,去掉重量平衡,虽然对于单个的 `find` 函数调用,时间复杂度依然是 O(1),但是对于 API 调用的整个过程,效率会有一定的下降。当然,好处就是减少了一些空间,不过对于 Big O 表示法来说,时空复杂度都没变。 - -下面言归正传,来看看这个算法有什么实际应用。 - -### 一、DFS 的替代方案 - -很多使用 DFS 深度优先算法解决的问题,也可以用 Union-Find 算法解决。 - -比如第 130 题,被围绕的区域:给你一个 M×N 的二维矩阵,其中包含字符 `X` 和 `O`,让你找到矩阵中**四面**被 `X` 围住的 `O`,并且把它们替换成 `X`。 - -```java -void solve(char[][] board); -``` - -注意哦,必须是四面被围的 `O` 才能被换成 `X`,也就是说边角上的 `O` 一定不会被围,进一步,与边角上的 `O` 相连的 `O` 也不会被 `X` 围四面,也不会被替换。 - -![](../pictures/unionfind应用/2.jpg) - -PS:这让我想起小时候玩的棋类游戏「黑白棋」,只要你用两个棋子把对方的棋子夹在中间,对方的子就被替换成你的子。可见,占据四角的棋子是无敌的,与其相连的边棋子也是无敌的(无法被夹掉)。 - -解决这个问题的传统方法也不困难,先用 for 循环遍历棋盘的**四边**,用 DFS 算法把那些与边界相连的 `O` 换成一个特殊字符,比如 `#`;然后再遍历整个棋盘,把剩下的 `O` 换成 `X`,把 `#` 恢复成 `O`。这样就能完成题目的要求,时间复杂度 O(MN)。 - -这个问题也可以用 Union-Find 算法解决,虽然实现复杂一些,甚至效率也略低,但这是使用 Union-Find 算法的通用思想,值得一学。 - -**你可以把那些不需要被替换的 `O` 看成一个拥有独门绝技的门派,它们有一个共同祖师爷叫 `dummy`,这些 `O` 和 `dummy` 互相连通,而那些需要被替换的 `O` 与 `dummy` 不连通**。 - -![](../pictures/unionfind应用/3.jpg) - -这就是 Union-Find 的核心思路,明白这个图,就很容易看懂代码了。 - -首先要解决的是,根据我们的实现,Union-Find 底层用的是一维数组,构造函数需要传入这个数组的大小,而题目给的是一个二维棋盘。 - -这个很简单,二维坐标 `(x,y)` 可以转换成 `x * n + y` 这个数(`m` 是棋盘的行数,`n` 是棋盘的列数)。敲黑板,**这是将二维坐标映射到一维的常用技巧**。 - -其次,我们之前描述的「祖师爷」是虚构的,需要给他老人家留个位置。索引 `[0.. m*n-1]` 都是棋盘内坐标的一维映射,那就让这个虚拟的 `dummy` 节点占据索引 `m * n` 好了。 - -```java -void solve(char[][] board) { - if (board.length == 0) return; - - int m = board.length; - int n = board[0].length; - // 给 dummy 留一个额外位置 - UF uf = new UF(m * n + 1); - int dummy = m * n; - // 将首列和末列的 O 与 dummy 连通 - for (int i = 0; i < m; i++) { - if (board[i][0] == 'O') - uf.union(i * n, dummy); - if (board[i][n - 1] == 'O') - uf.union(i * n + n - 1, dummy); - } - // 将首行和末行的 O 与 dummy 连通 - for (int j = 0; j < n; j++) { - if (board[0][j] == 'O') - uf.union(j, dummy); - if (board[m - 1][j] == 'O') - uf.union(n * (m - 1) + j, dummy); - } - // 方向数组 d 是上下左右搜索的常用手法 - int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}}; - for (int i = 1; i < m - 1; i++) - for (int j = 1; j < n - 1; j++) - if (board[i][j] == 'O') - // 将此 O 与上下左右的 O 连通 - for (int k = 0; k < 4; k++) { - int x = i + d[k][0]; - int y = j + d[k][1]; - if (board[x][y] == 'O') - uf.union(x * n + y, i * n + j); - } - // 所有不和 dummy 连通的 O,都要被替换 - for (int i = 1; i < m - 1; i++) - for (int j = 1; j < n - 1; j++) - if (!uf.connected(dummy, i * n + j)) - board[i][j] = 'X'; -} -``` - -这段代码很长,其实就是刚才的思路实现,只有和边界 `O` 相连的 `O` 才具有和 `dummy` 的连通性,他们不会被替换。 - -说实话,Union-Find 算法解决这个简单的问题有点杀鸡用牛刀,它可以解决更复杂,更具有技巧性的问题,**主要思路是适时增加虚拟节点,想办法让元素「分门别类」,建立动态连通关系**。 - -### 二、判定合法等式 - -这个问题用 Union-Find 算法就显得十分优美了。题目是这样: - -给你一个数组 `equations`,装着若干字符串表示的算式。每个算式 `equations[i]` 长度都是 4,而且只有这两种情况:`a==b` 或者 `a!=b`,其中 `a,b` 可以是任意小写字母。你写一个算法,如果 `equations` 中所有算式都不会互相冲突,返回 true,否则返回 false。 - -比如说,输入 `["a==b","b!=c","c==a"]`,算法返回 false,因为这三个算式不可能同时正确。 - -再比如,输入 `["c==c","b==d","x!=z"]`,算法返回 true,因为这三个算式并不会造成逻辑冲突。 - -我们前文说过,动态连通性其实就是一种等价关系,具有「自反性」「传递性」和「对称性」,其实 `==` 关系也是一种等价关系,具有这些性质。所以这个问题用 Union-Find 算法就很自然。 - -核心思想是,**将 `equations` 中的算式根据 `==` 和 `!=` 分成两部分,先处理 `==` 算式,使得他们通过相等关系各自勾结成门派;然后处理 `!=` 算式,检查不等关系是否破坏了相等关系的连通性**。 - -```java -boolean equationsPossible(String[] equations) { - // 26 个英文字母 - UF uf = new UF(26); - // 先让相等的字母形成连通分量 - for (String eq : equations) { - if (eq.charAt(1) == '=') { - char x = eq.charAt(0); - char y = eq.charAt(3); - uf.union(x - 'a', y - 'a'); - } - } - // 检查不等关系是否打破相等关系的连通性 - for (String eq : equations) { - if (eq.charAt(1) == '!') { - char x = eq.charAt(0); - char y = eq.charAt(3); - // 如果相等关系成立,就是逻辑冲突 - if (uf.connected(x - 'a', y - 'a')) - return false; - } - } - return true; -} -``` - -至此,这道判断算式合法性的问题就解决了,借助 Union-Find 算法,是不是很简单呢? - -### 三、简单总结 - -使用 Union-Find 算法,主要是如何把原问题转化成图的动态连通性问题。对于算式合法性问题,可以直接利用等价关系,对于棋盘包围问题,则是利用一个虚拟节点,营造出动态连通特性。 - -另外,将二维数组映射到一维数组,利用方向数组 `d` 来简化代码量,都是在写算法时常用的一些小技巧,如果没见过可以注意一下。 - -很多更复杂的 DFS 算法问题,都可以利用 Union-Find 算法更漂亮的解决。LeetCode 上 Union-Find 相关的问题也就二十多道,有兴趣的读者可以去做一做。 - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:Union-Find算法详解](../算法思维系列/UnionFind算法详解.md) - -[下一篇:一行代码就能解决的算法题](../高频面试系列/一行代码解决的智力题.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" index 3ff8cf4c81..c6e4be4cbd 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" @@ -1,66 +1,97 @@ -# Union-Find算法详解 +# Union-Find 并查集算法 -今天讲讲 Union-Find 算法,也就是常说的并查集算法,主要是解决图论中「动态连通性」问题的。名词很高端,其实特别好理解,等会解释,另外这个算法的应用都非常有趣。 -说起这个 Union-Find,应该算是我的「启蒙算法」了,因为《算法4》的开头就介绍了这款算法,可是把我秀翻了,感觉好精妙啊!后来刷了 LeetCode,并查集相关的算法题目都非常有意思,而且《算法4》给的解法竟然还可以进一步优化,只要加一个微小的修改就可以把时间复杂度降到 O(1)。 -废话不多说,直接上干货,先解释一下什么叫动态连通性吧。 +![](https://labuladong.online/algo/images/souyisou1.png) -### 一、问题介绍 +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [130. Surrounded Regions](https://leetcode.com/problems/surrounded-regions/) | [130. 被围绕的区域](https://leetcode.cn/problems/surrounded-regions/) | 🟠 | +| [323. Number of Connected Components in an Undirected Graph](https://leetcode.com/problems/number-of-connected-components-in-an-undirected-graph/)🔒 | [323. 无向图中连通分量的数目](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/)🔒 | 🟠 | +| [684. Redundant Connection](https://leetcode.com/problems/redundant-connection/) | [684. 冗余连接](https://leetcode.cn/problems/redundant-connection/) | 🟠 | +| [990. Satisfiability of Equality Equations](https://leetcode.com/problems/satisfiability-of-equality-equations/) | [990. 等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [多叉树基础及遍历](https://labuladong.online/algo/data-structure-basic/n-ary-tree-traverse-basic/) +> - [图结构基础及通用实现](https://labuladong.online/algo/data-structure-basic/graph-basic/) + +并查集(Union-Find)算法是一个专门针对「动态连通性」的算法,我之前写过两次,因为这个算法的考察频率高,而且它也是最小生成树算法的前置知识,所以我整合了本文,争取一篇文章把这个算法讲明白。 + +首先,从什么是图的动态连通性开始讲。 + +## 一、动态连通性 简单说,动态连通性其实可以抽象成给一幅图连线。比如下面这幅图,总共有 10 个节点,他们互不相连,分别用 0~9 标记: -![](../pictures/unionfind/1.jpg) +![](https://labuladong.online/algo/images/unionfind/1.jpg) 现在我们的 Union-Find 算法主要需要实现这两个 API: ```java class UF { - /* 将 p 和 q 连接 */ + // 将 p 和 q 连接 public void union(int p, int q); - /* 判断 p 和 q 是否连通 */ + // 判断 p 和 q 是否连通 public boolean connected(int p, int q); - /* 返回图中有多少个连通分量 */ + // 返回图中有多少个连通分量 public int count(); } ``` 这里所说的「连通」是一种等价关系,也就是说具有如下三个性质: -1、自反性:节点`p`和`p`是连通的。 +1、自反性:节点 `p` 和 `p` 是连通的。 -2、对称性:如果节点`p`和`q`连通,那么`q`和`p`也连通。 +2、对称性:如果节点 `p` 和 `q` 连通,那么 `q` 和 `p` 也连通。 -3、传递性:如果节点`p`和`q`连通,`q`和`r`连通,那么`p`和`r`也连通。 +3、传递性:如果节点 `p` 和 `q` 连通,`q` 和 `r` 连通,那么 `p` 和 `r` 也连通。 -比如说之前那幅图,0~9 任意两个**不同**的点都不连通,调用`connected`都会返回 false,连通分量为 10 个。 +比如说之前那幅图,0~9 任意两个**不同**的点都不连通,调用 `connected` 都会返回 false,连通分量为 10 个。 -如果现在调用`union(0, 1)`,那么 0 和 1 被连通,连通分量降为 9 个。 +如果现在调用 `union(0, 1)`,那么 0 和 1 被连通,连通分量降为 9 个。 -再调用`union(1, 2)`,这时 0,1,2 都被连通,调用`connected(0, 2)`也会返回 true,连通分量变为 8 个。 +再调用 `union(1, 2)`,这时 0,1,2 都被连通,调用 `connected(0, 2)` 也会返回 true,连通分量变为 8 个。 -![](../pictures/unionfind/2.jpg) +![](https://labuladong.online/algo/images/unionfind/2.jpg) 判断这种「等价关系」非常实用,比如说编译器判断同一个变量的不同引用,比如社交网络中的朋友圈计算等等。 -这样,你应该大概明白什么是动态连通性了,Union-Find 算法的关键就在于`union`和`connected`函数的效率。那么用什么模型来表示这幅图的连通状态呢?用什么数据结构来实现代码呢? +这样,你应该大概明白什么是动态连通性了,Union-Find 算法的关键就在于 `union` 和 `connected` 函数的效率。那么用什么模型来表示这幅图的连通状态呢?用什么数据结构来实现代码呢? + + + + -### 二、基本思路 + + +## 二、基本思路 注意我刚才把「模型」和具体的「数据结构」分开说,这么做是有原因的。因为我们使用森林(若干棵树)来表示图的动态连通性,用数组来具体实现这个森林。 怎么用森林来表示连通性呢?我们设定树的每个节点有一个指针指向其父节点,如果是根节点的话,这个指针指向自己。比如说刚才那幅 10 个节点的图,一开始的时候没有相互连通,就是这样: -![](../pictures/unionfind/3.jpg) +![](https://labuladong.online/algo/images/unionfind/3.jpg) ```java class UF { // 记录连通分量 private int count; - // 节点 x 的节点是 parent[x] + // 节点 x 的父节点是 parent[x] private int[] parent; - /* 构造函数,n 为图的节点总数 */ + // 构造函数,n 为图的节点总数 public UF(int n) { // 一开始互不连通 this.count = n; @@ -70,85 +101,108 @@ class UF { parent[i] = i; } - /* 其他函数 */ + // 其他函数 } ``` **如果某两个节点被连通,则让其中的(任意)一个节点的根节点接到另一个节点的根节点上**: -![](../pictures/unionfind/4.jpg) + + + + +![](https://labuladong.online/algo/images/unionfind/4.jpg) ```java -public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - // 将两棵树合并为一棵 - parent[rootP] = rootQ; - // parent[rootQ] = rootP 也一样 - count--; // 两个分量合二为一 -} +class UF { + // 为了节约篇幅,省略上文给出的代码部分... -/* 返回某个节点 x 的根节点 */ -private int find(int x) { - // 根节点的 parent[x] == x - while (parent[x] != x) - x = parent[x]; - return x; -} + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + // 将两棵树合并为一棵 + parent[rootP] = rootQ; + // parent[rootQ] = rootP 也一样 + + // 两个分量合二为一 + count--; + } + + // 返回某个节点 x 的根节点 + private int find(int x) { + // 根节点的 parent[x] == x + while (parent[x] != x) + x = parent[x]; + return x; + } -/* 返回当前的连通分量个数 */ -public int count() { - return count; + // 返回当前的连通分量个数 + public int count() { + return count; + } } ``` -**这样,如果节点`p`和`q`连通的话,它们一定拥有相同的根节点**: +**这样,如果节点 `p` 和 `q` 连通的话,它们一定拥有相同的根节点**: + + + -![](../pictures/unionfind/5.jpg) + +![](https://labuladong.online/algo/images/unionfind/5.jpg) ```java -public boolean connected(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - return rootP == rootQ; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } } ``` 至此,Union-Find 算法就基本完成了。是不是很神奇?竟然可以这样使用数组来模拟出一个森林,如此巧妙的解决这个比较复杂的问题! -那么这个算法的复杂度是多少呢?我们发现,主要 API`connected`和`union`中的复杂度都是`find`函数造成的,所以说它们的复杂度和`find`一样。 +那么这个算法的复杂度是多少呢?我们发现,主要 API `connected` 和 `union` 中的复杂度都是 `find` 函数造成的,所以说它们的复杂度和 `find` 一样。 -`find`主要功能就是从某个节点向上遍历到树根,其时间复杂度就是树的高度。我们可能习惯性地认为树的高度就是`logN`,但这并不一定。`logN`的高度只存在于平衡二叉树,对于一般的树可能出现极端不平衡的情况,使得「树」几乎退化成「链表」,树的高度最坏情况下可能变成`N`。 +`find` 主要功能就是从某个节点向上遍历到树根,其时间复杂度就是树的高度。我们可能习惯性地认为树的高度就是 `logN`,但这并不一定。`logN` 的高度只存在于平衡二叉树,对于一般的树可能出现极端不平衡的情况,使得「树」几乎退化成「链表」,树的高度最坏情况下可能变成 `N`。 -![](../pictures/unionfind/6.jpg) +![](https://labuladong.online/algo/images/unionfind/6.jpg) -所以说上面这种解法,`find`,`union`,`connected`的时间复杂度都是 O(N)。这个复杂度很不理想的,你想图论解决的都是诸如社交网络这样数据规模巨大的问题,对于`union`和`connected`的调用非常频繁,每次调用需要线性时间完全不可忍受。 +所以说上面这种解法,`find` , `union` , `connected` 的时间复杂度都是 O(N)。这个复杂度很不理想的,你想图论解决的都是诸如社交网络这样数据规模巨大的问题,对于 `union` 和 `connected` 的调用非常频繁,每次调用需要线性时间完全不可忍受。 **问题的关键在于,如何想办法避免树的不平衡呢**?只需要略施小计即可。 -### 三、平衡性优化 +## 三、平衡性优化 -我们要知道哪种情况下可能出现不平衡现象,关键在于`union`过程: +我们要知道哪种情况下可能出现不平衡现象,关键在于 `union` 过程: ```java -public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - // 将两棵树合并为一棵 - parent[rootP] = rootQ; - // parent[rootQ] = rootP 也可以 - count--; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + // 将两棵树合并为一棵 + parent[rootP] = rootQ; + // parent[rootQ] = rootP 也可以 + count--; + } +} ``` -我们一开始就是简单粗暴的把`p`所在的树接到`q`所在的树的根节点下面,那么这里就可能出现「头重脚轻」的不平衡状况,比如下面这种局面: +我们一开始就是简单粗暴的把 `p` 所在的树接到 `q` 所在的树的根节点下面,那么这里就可能出现「头重脚轻」的不平衡状况,比如下面这种局面: -![](../pictures/unionfind/7.jpg) +![](https://labuladong.online/algo/images/unionfind/7.jpg) -长此以往,树可能生长得很不平衡。**我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些**。解决方法是额外使用一个`size`数组,记录每棵树包含的节点数,我们不妨称为「重量」: +长此以往,树可能生长得很不平衡。**我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些**。解决方法是额外使用一个 `size` 数组,记录每棵树包含的节点数,我们不妨称为「重量」: ```java class UF { @@ -168,133 +222,239 @@ class UF { size[i] = 1; } } - /* 其他函数 */ + // 其他函数 } ``` -比如说`size[3] = 5`表示,以节点`3`为根的那棵树,总共有`5`个节点。这样我们可以修改一下`union`方法: +比如说 `size[3] = 5` 表示,以节点 `3` 为根的那棵树,总共有 `5` 个节点。这样我们可以修改一下 `union` 方法: ```java -public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - - // 小树接到大树下面,较平衡 - if (size[rootP] > size[rootQ]) { - parent[rootQ] = rootP; - size[rootP] += size[rootQ]; - } else { - parent[rootP] = rootQ; - size[rootQ] += size[rootP]; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + count--; } - count--; } ``` -这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在`logN`这个数量级,极大提升执行效率。 +这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在 `logN` 这个数量级,极大提升执行效率。 + +此时,`find` , `union` , `connected` 的时间复杂度都下降为 O(logN),即便数据规模上亿,所需时间也非常少。 + +## 四、路径压缩 -此时,`find`,`union`,`connected`的时间复杂度都下降为 O(logN),即便数据规模上亿,所需时间也非常少。 +这步优化虽然代码很简单,但原理非常巧妙。 -### 四、路径压缩 +**其实我们并不在乎每棵树的结构长什么样,只在乎根节点**。 -这步优化特别简单,所以非常巧妙。我们能不能进一步压缩每棵树的高度,使树高始终保持为常数? +因为无论树长啥样,树上的每个节点的根节点都是相同的,所以能不能进一步压缩每棵树的高度,使树高始终保持为常数? -![](../pictures/unionfind/8.jpg) +![](https://labuladong.online/algo/images/unionfind/8.jpg) -这样`find`就能以 O(1) 的时间找到某一节点的根节点,相应的,`connected`和`union`复杂度都下降为 O(1)。 +这样每个节点的父节点就是整棵树的根节点,`find` 就能以 O(1) 的时间找到某一节点的根节点,相应的,`connected` 和 `union` 复杂度都下降为 O(1)。 -要做到这一点,非常简单,只需要在`find`中加一行代码: +要做到这一点主要是修改 `find` 函数逻辑,非常简单,但你可能会看到两种不同的写法。 + +第一种是在 `find` 中加一行代码: ```java -private int find(int x) { - while (parent[x] != x) { - // 进行路径压缩 - parent[x] = parent[parent[x]]; - x = parent[x]; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + private int find(int x) { + while (parent[x] != x) { + // 这行代码进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; } - return x; } ``` 这个操作有点匪夷所思,看个 GIF 就明白它的作用了(为清晰起见,这棵树比较极端): -![](../pictures/unionfind/9.gif) +![](https://labuladong.online/algo/images/unionfind/9.gif) + +用语言描述就是,每次 while 循环都会让部分子节点向上移动,这样每次调用 `find` 函数向树根遍历的同时,顺手就将树高缩短了。 + +路径压缩的第二种写法是这样: -可见,调用`find`函数每次向树根遍历的同时,顺手将树高缩短了,最终所有树高都不会超过 3(`union`的时候树高可能达到 3)。 +```java +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + // 第二种路径压缩的 find 方法 + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } +} +``` -PS:读者可能会问,这个 GIF 图的find过程完成之后,树高恰好等于 3 了,但是如果更高的树,压缩后高度依然会大于 3 呀?不能这么想。这个 GIF 的情景是我编出来方便大家理解路径压缩的,但是实际中,每次find都会进行路径压缩,所以树本来就不可能增长到这么高,你的这种担心应该是多余的。 +我一度认为这种递归写法和第一种迭代写法做的事情一样,但实际上是我大意了,有读者指出这种写法进行路径压缩的效率是高于上一种解法的。 -### 五、最后总结 +这个递归过程有点不好理解,你可以自己手画一下递归过程。我把这个函数做的事情翻译成迭代形式,方便你理解它进行路径压缩的原理: -我们先来看一下完整代码: +```java +// 这段迭代代码方便你理解递归代码所做的事情 +public int find(int x) { + // 先找到根节点 + int root = x; + while (parent[root] != root) { + root = parent[root]; + } + // 然后把 x 到根节点之间的所有节点直接接到根节点下面 + int old_parent = parent[x]; + while (x != root) { + parent[x] = root; + x = old_parent; + old_parent = parent[old_parent]; + } + return root; +} +``` + +这种路径压缩的效果如下: + +![](https://labuladong.online/algo/images/unionfind/10.jpeg) + +比起第一种路径压缩,显然这种方法压缩得更彻底,直接把一整条树枝压平,一点意外都没有。就算一些极端情况下产生了一棵比较高的树,只要一次路径压缩就能大幅降低树高,从 [摊还分析](https://labuladong.online/algo/essential-technique/complexity-analysis/) 的角度来看,所有操作的平均时间复杂度依然是 O(1),所以从效率的角度来说,推荐你使用这种路径压缩算法。 + +**另外,如果使用路径压缩技巧,那么 `size` 数组的平衡优化就没有必要了**。所以你一般看到的 Union Find 算法应该是如下实现: ```java class UF { // 连通分量个数 private int count; - // 存储一棵树 + // 存储每个节点的父节点 private int[] parent; - // 记录树的“重量” - private int[] size; + // n 为图中节点的个数 public UF(int n) { this.count = n; parent = new int[n]; - size = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; - size[i] = 1; } } + // 将节点 p 和节点 q 连通 public void union(int p, int q) { int rootP = find(p); int rootQ = find(q); + if (rootP == rootQ) return; - // 小树接到大树下面,较平衡 - if (size[rootP] > size[rootQ]) { - parent[rootQ] = rootP; - size[rootP] += size[rootQ]; - } else { - parent[rootP] = rootQ; - size[rootQ] += size[rootP]; - } + parent[rootQ] = rootP; + // 两个连通分量合并成一个连通分量 count--; } + // 判断节点 p 和节点 q 是否连通 public boolean connected(int p, int q) { int rootP = find(p); int rootQ = find(q); return rootP == rootQ; } - private int find(int x) { - while (parent[x] != x) { - // 进行路径压缩 - parent[x] = parent[parent[x]]; - x = parent[x]; + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); } - return x; + return parent[x]; } + // 返回图中的连通分量个数 public int count() { return count; } } ``` -Union-Find 算法的复杂度可以这样分析:构造函数初始化数据结构需要 O(N) 的时间和空间复杂度;连通两个节点`union`、判断两个节点的连通性`connected`、计算连通分量`count`所需的时间复杂度均为 O(1)。 +Union-Find 算法的复杂度可以这样分析:构造函数初始化数据结构需要 O(N) 的时间和空间复杂度;连通两个节点 `union`、判断两个节点的连通性 `connected`、计算连通分量 `count` 所需的时间复杂度均为 O(1)。 + +到这里,相信你已经掌握了 Union-Find 算法的核心逻辑,总结一下我们优化算法的过程: + +1、用 `parent` 数组记录每个节点的父节点,相当于指向父节点的指针,所以 `parent` 数组内实际存储着一个森林(若干棵多叉树)。 + +2、用 `size` 数组记录着每棵树的重量,目的是让 `union` 后树依然拥有平衡性,保证各个 API 时间复杂度为 O(logN),而不会退化成链表影响操作效率。 + +3、在 `find` 函数中进行路径压缩,保证任意树的高度保持在常数,使得各个 API 时间复杂度为 O(1)。使用了路径压缩之后,可以不使用 `size` 数组的平衡优化。 + +> [!TIP] +> 大部分笔试都是允许你使用自己的 IDE 编码的,所以你可以提前把这个 `UF` 类用你熟悉的编程语言写好,笔试需要时直接拿来用。它的代码量稍微有点多,没必要现场从头写。 + + + + + + + +
+
+引用本文的文章 + + - [Kruskal 最小生成树算法](https://labuladong.online/algo/data-structure/kruskal/) + - [Prim 最小生成树算法](https://labuladong.online/algo/data-structure/prim/) + - [Union Find 并查集原理](https://labuladong.online/algo/data-structure-basic/union-find-basic/) + - [【强化练习】BFS 经典习题 II](https://labuladong.online/algo/problem-set/bfs-ii/) + - [【强化练习】并查集经典习题](https://labuladong.online/algo/problem-set/union-find/) + - [【强化练习】运用层序遍历解题 II](https://labuladong.online/algo/problem-set/binary-tree-level-ii/) + - [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) + - [二叉树基础及常见类型](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [用算法打败算法](https://labuladong.online/algo/fname.html?fname=PDF中的算法) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1361. Validate Binary Tree Nodes](https://leetcode.com/problems/validate-binary-tree-nodes/?show=1) | [1361. 验证二叉树](https://leetcode.cn/problems/validate-binary-tree-nodes/?show=1) | 🟠 | +| [200. Number of Islands](https://leetcode.com/problems/number-of-islands/?show=1) | [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/?show=1) | 🟠 | +| [261. Graph Valid Tree](https://leetcode.com/problems/graph-valid-tree/?show=1)🔒 | [261. 以图判树](https://leetcode.cn/problems/graph-valid-tree/?show=1)🔒 | 🟠 | +| [310. Minimum Height Trees](https://leetcode.com/problems/minimum-height-trees/?show=1) | [310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/?show=1) | 🟠 | +| [368. Largest Divisible Subset](https://leetcode.com/problems/largest-divisible-subset/?show=1) | [368. 最大整除子集](https://leetcode.cn/problems/largest-divisible-subset/?show=1) | 🟠 | +| [547. Number of Provinces](https://leetcode.com/problems/number-of-provinces/?show=1) | [547. 省份数量](https://leetcode.cn/problems/number-of-provinces/?show=1) | 🟠 | +| [582. Kill Process](https://leetcode.com/problems/kill-process/?show=1)🔒 | [582. 杀掉进程](https://leetcode.cn/problems/kill-process/?show=1)🔒 | 🟠 | +| [737. Sentence Similarity II](https://leetcode.com/problems/sentence-similarity-ii/?show=1)🔒 | [737. 句子相似性 II](https://leetcode.cn/problems/sentence-similarity-ii/?show=1)🔒 | 🟠 | +| [765. Couples Holding Hands](https://leetcode.com/problems/couples-holding-hands/?show=1) | [765. 情侣牵手](https://leetcode.cn/problems/couples-holding-hands/?show=1) | 🔴 | +| [924. Minimize Malware Spread](https://leetcode.com/problems/minimize-malware-spread/?show=1) | [924. 尽量减少恶意软件的传播](https://leetcode.cn/problems/minimize-malware-spread/?show=1) | 🔴 | +| [947. Most Stones Removed with Same Row or Column](https://leetcode.com/problems/most-stones-removed-with-same-row-or-column/?show=1) | [947. 移除最多的同行或同列石头](https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/?show=1) | 🟠 | + +
+
+ -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:如何调度考生的座位](../高频面试系列/座位调度.md) -[下一篇:Union-Find算法应用](../算法思维系列/UnionFind算法应用.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" deleted file mode 100644 index 00ba39d261..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" +++ /dev/null @@ -1,165 +0,0 @@ -# twoSum问题的核心思想 - -Two Sum 系列问题在 LeetCode 上有好几道,这篇文章就挑出有代表性的几道,介绍一下这种问题怎么解决。 - -### TwoSum I - -这个问题的**最基本形式**是这样:给你一个数组和一个整数 `target`,可以保证数组中**存在**两个数的和为 `target`,请你返回这两个数的索引。 - -比如输入 `nums = [3,1,3,6], target = 6`,算法应该返回数组 `[0,2]`,因为 3 + 3 = 6。 - -这个问题如何解决呢?首先最简单粗暴的办法当然是穷举了: - -```java -int[] twoSum(int[] nums, int target) { - - for (int i = 0; i < nums.length; i++) - for (int j = i + 1; j < nums.length; j++) - if (nums[j] == target - nums[i]) - return new int[] { i, j }; - - // 不存在这么两个数 - return new int[] {-1, -1}; -} -``` - -这个解法非常直接,时间复杂度 O(N^2),空间复杂度 O(1)。 - -可以通过一个哈希表减少时间复杂度: - -```java -int[] twoSum(int[] nums, int target) { - int n = nums.length; - index index = new HashMap<>(); - // 构造一个哈希表:元素映射到相应的索引 - for (int i = 0; i < n; i++) - index.put(nums[i], i); - - for (int i = 0; i < n; i++) { - int other = target - nums[i]; - // 如果 other 存在且不是 nums[i] 本身 - if (index.containsKey(other) && index.get(other) != i) - return new int[] {i, index.get(other)}; - } - - return new int[] {-1, -1}; -} -``` - -这样,由于哈希表的查询时间为 O(1),算法的时间复杂度降低到 O(N),但是需要 O(N) 的空间复杂度来存储哈希表。不过综合来看,是要比暴力解法高效的。 - -**我觉得 Two Sum 系列问题就是想教我们如何使用哈希表处理问题**。我们接着往后看。 - -### TwoSum II - -这里我们稍微修改一下上面的问题。我们设计一个类,拥有两个 API: - -```java -class TwoSum { - // 向数据结构中添加一个数 number - public void add(int number); - // 寻找当前数据结构中是否存在两个数的和为 value - public boolean find(int value); -} -``` - -如何实现这两个 API 呢,我们可以仿照上一道题目,使用一个哈希表辅助 `find` 方法: - -```java -class TwoSum { - Map freq = new HashMap<>(); - - public void add(int number) { - // 记录 number 出现的次数 - freq.put(number, freq.getOrDefault(number, 0) + 1); - } - - public boolean find(int value) { - for (Integer key : freq.keySet()) { - int other = value - key; - // 情况一 - if (other == key && freq.get(key) > 1) - return true; - // 情况二 - if (other != key && freq.containsKey(other)) - return true; - } - return false; - } -} -``` - -进行 `find` 的时候有两种情况,举个例子: - -情况一:`add` 了 `[3,3,2,5]` 之后,执行 `find(6)`,由于 3 出现了两次,3 + 3 = 6,所以返回 true。 - -情况二:`add` 了 `[3,3,2,5]` 之后,执行 `find(7)`,那么 `key` 为 2,`other` 为 5 时算法可以返回 true。 - -除了上述两种情况外,`find` 只能返回 false 了。 - -对于这个解法的时间复杂度呢,`add` 方法是 O(1),`find` 方法是 O(N),空间复杂度为 O(N),和上一道题目比较类似。 - -**但是对于 API 的设计,是需要考虑现实情况的**。比如说,我们设计的这个类,使用 `find` 方法非常频繁,那么每次都要 O(N) 的时间,岂不是很浪费费时间吗?对于这种情况,我们是否可以做些优化呢? - -是的,对于频繁使用 `find` 方法的场景,我们可以进行优化。我们可以参考上一道题目的暴力解法,借助**哈希集合**来针对性优化 `find` 方法: - -```java -class TwoSum { - Set sum = new HashSet<>(); - List nums = new ArrayList<>(); - - public void add(int number) { - // 记录所有可能组成的和 - for (int n : nums) - sum.add(n + number); - nums.add(number); - } - - public boolean find(int value) { - return sum.contains(value); - } -} -``` - -这样 `sum` 中就储存了所有加入数字可能组成的和,每次 `find` 只要花费 O(1) 的时间在集合中判断一下是否存在就行了,显然非常适合频繁使用 `find` 的场景。 - -### 三、总结 - -对于 TwoSum 问题,一个难点就是给的数组**无序**。对于一个无序的数组,我们似乎什么技巧也没有,只能暴力穷举所有可能。 - -**一般情况下,我们会首先把数组排序再考虑双指针技巧**。TwoSum 启发我们,HashMap 或者 HashSet 也可以帮助我们处理无序数组相关的简单问题。 - -另外,设计的核心在于权衡,利用不同的数据结构,可以得到一些针对性的加强。 - -最后,如果 TwoSum I 中给的数组是有序的,应该如何编写算法呢?答案很简单,前文「双指针技巧汇总」写过: - -```java -int[] twoSum(int[] nums, int target) { - int left = 0, right = nums.length - 1; - while (left < right) { - int sum = nums[left] + nums[right]; - if (sum == target) { - return new int[]{left, right}; - } else if (sum < target) { - left++; // 让 sum 大一点 - } else if (sum > target) { - right--; // 让 sum 小一点 - } - } - // 不存在这样两个数 - return new int[]{-1, -1}; -} -``` - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:滑动窗口技巧](../算法思维系列/滑动窗口技巧.md) - -[下一篇:常用的位操作](../算法思维系列/常用的位操作.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" deleted file mode 100644 index c400bd528c..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" +++ /dev/null @@ -1,82 +0,0 @@ -# 为什么我推荐《算法4》 - -咱们的公众号有很多硬核的算法文章,今天就聊点轻松的,就具体聊聊我非常“鼓吹”的《算法4》。这本书我在之前的文章多次推荐过,但是没有具体的介绍,今天就来正式介绍一下。。 - -我的推荐不会直接甩一大堆书目,而是会联系实际生活,讲一些书中有趣有用的知识,无论你最后会不会去看这本书,本文都会给你带来一些收获。 - -**首先这本书是适合初学者的**。总是有很多读者问,我只会 C 语言,能不能看《算法4》?学算法最好用什么语言?诸如此类的问题。 - -经常看咱们公众号的读者应该体会到了,算法其实是一种思维模式,和你用什么语言没啥关系。我们的文章也不会固定用某一种语言,而是什么语言写出来容易理解就用什么语言。再退一步说,到底适不适合你,网上找个 PDF 亲自看一下不就知道了? - -《算法4》看起来挺厚的,但是前面几十页是教你 Java 的;每章后面还有习题,占了不少页数;每章还有一些数学证明,这些都可以忽略。这样算下来,剩下的就是基础知识和疑难解答之类的内容,含金量很高,把这些基础知识动手实践一遍,真的就可以达到不错的水平了。 - -我觉得这本书之所以能有这么高的评分,一个是因为讲解详细,还有大量配图,另一个原因就是书中把一些算法和现实生活中的使用场景联系起来,你不仅知道某个算法怎么实现,也知道它大概能运用到什么场景,下面我就来介绍两个图算法的简单应用。 - -### 一、二分图的应用 - -我想举的第一个例子是**二分图**。简单来说,二分图就是一幅拥有特殊性质的图:能够用两种颜色为所有顶点着色,使得任何一条边的两个顶点颜色不同。 - -![](../pictures/algo4/1.jpg) - -明白了二分图是什么,能解决什么实际问题呢?**算法方面,常见的操作是如何判定一幅图是不是二分图**。比如说下面这道 LeetCode 题目: - -![](../pictures/algo4/title.png) - -你想想,如果我们把每个人视为一个顶点,边代表讨厌;相互讨厌的两个人之间连接一条边,就可以形成一幅图。那么根据刚才二分图的定义,如果这幅图是一幅二分图,就说明这些人可以被分为两组,否则的话就不行。 - -这是判定二分图算法的一个应用,**其实二分图在数据结构方面也有一些不错的特性**。 - -比如说我们需要一种数据结构来储存电影和演员之间的关系:某一部电影肯定是由多位演员出演的,且某一位演员可能会出演多部电影。你使用什么数据结构来存储这种关系呢? - -既然是存储映射关系,最简单的不就是使用哈希表嘛,我们可以使用一个 `HashMap>` 来存储电影到演员列表的映射,如果给一部电影的名字,就能快速得到出演该电影的演员。 - -但是如果给出一个演员的名字,我们想快速得到该演员演出的所有电影,怎么办呢?这就需要「反向索引」,对之前的哈希表进行一些操作,新建另一个哈希表,把演员作为键,把电影列表作为值。 - -对于上面这个例子,可以使用二分图来取代哈希表。电影和演员是具有二分图性质的:如果把电影和演员视为图中的顶点,出演关系作为边,那么与电影顶点相连的一定是演员,与演员相邻的一定是电影,不存在演员和演员相连,电影和电影相连的情况。 - -回顾二分图的定义,如果对演员和电影顶点着色,肯定就是一幅二分图: - -![](../pictures/algo4/2.jpg) - -如果这幅图构建完成,就不需要反向索引,对于演员顶点,其直接连接的顶点就是他出演的电影,对于电影顶点,其直接连接的顶点就是出演演员。 - -当然,对于这个问题,书中还提到了一些其他有趣的玩法,比如说社交网络中「间隔度数」的计算(六度空间理论应该听说过)等等,其实就是一个 BFS 广度优先搜索寻找最短路径的问题,具体代码实现这里就不展开了。 - -### 二、套汇的算法 - -如果我们说货币 A 到货币 B 的汇率是 10,意思就是 1 单位的货币 A 可以换 10 单位货币 B。如果我们把每种货币视为一幅图的顶点,货币之间的汇率视为加权有向边,那么整个汇率市场就是一幅「完全加权有向图」。 - -一旦把现实生活中的情景抽象成图,就有可能运用算法解决一些问题。比如说图中可能存在下面的情况: - -![](../pictures/algo4/3.jpg) - -图中的加权有向边代表汇率,我们可以发现如果把 100 单位的货币 A 换成 B,再换成 C,最后换回 A,就可以得到 100×0.9×0.8×1.4 = 100.8 单位的 A!如果交易的金额大一些的话,赚的钱是很可观的,这种空手套白狼的操作就是套汇。 - -现实中交易会有种种限制,而且市场瞬息万变,但是套汇的利润还是很高的,关键就在于如何**快速**找到这种套汇机会呢? - -借助图的抽象,我们发现套汇机会其实就是一个环,且这个环上的权重之积大于 1,只要在顺着这个环交易一圈就能空手套白狼。 - -图论中有一个经典算法叫做 **Bellman-Ford 算法,可以用于寻找负权重环**。对于我们说的套汇问题,可以先把所有边的权重 w 替换成 -ln(w),这样「寻找权重乘积大于 1 的环」就转化成了「寻找权重和小于 0 的环」,就可以使用 Bellman-Ford 算法在 O(EV) 的时间内寻找负权重环,也就是寻找套汇机会。 - -《算法4》就介绍到这里,关于上面两个例子的具体内容,可以自己去看书,公众号后台回复关键词「算法4」就有 PDF。 - - -### 三、最后说几句 - -首先,前文说对于数学证明、章后习题可以忽略,可能有人要抬杠了:难道习题和数学证明不重要吗? - -那我想说,就是不重要,起码对大多数人来说不重要。我觉得吧,学习就要带着目的性去学,大部分人学算法不就是巩固计算机知识,对付面试题目吗?**如果是这个目的**,那就学些基本的数据结构和经典算法,明白它们的时间复杂度,然后去刷题就好了,何必和习题、证明过不去? - -这也是我从来不推荐《算法导论》这本书的原因。如果有人给你推荐这本书,只可能有两个原因,要么他是真大佬,要么他在装大佬。《算法导论》中充斥大量数学证明,而且很多数据结构是很少用到的,顶多当个字典用。你说你学了那些有啥用呢,饶过自己呗。 - -另外,读书在精不在多。你花时间《算法4》过个大半(最后小半部分有点困难),同时刷点题,看看咱们的公众号文章,算法这块真就够了,别对细节问题太较真。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**,公众号后台回复关键词「算法4」可以获得 PDF 下载: - -![labuladong](../pictures/labuladong.png) - -[上一篇:学习算法和刷题的框架思维](../算法思维系列/学习数据结构和算法的高效方法.md) - -[下一篇:动态规划解题框架](../动态规划系列/动态规划详解进阶.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" index ea1fd19e95..d03e5e7c85 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" @@ -1,27 +1,68 @@ -# 二分查找详解 +# 二分搜索算法核心代码模板 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [34. Find First and Last Position of Element in Sorted Array](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/) | [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) | 🟠 | +| [704. Binary Search](https://leetcode.com/problems/binary-search/) | [704. 二分查找](https://leetcode.cn/problems/binary-search/) | 🟢 | +| - | [剑指 Offer 53 - I. 在排序数组中查找数字 I](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) + +> tip:本文有视频版:[二分搜索核心框架套路](https://www.bilibili.com/video/BV1Gt4y1b79Q/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +本文是旧文 [二分搜索详解](https://mp.weixin.qq.com/s/uA2suoVykENmCQcKFMOSuQ) 的修订版,添加了对二分搜索算法更详细的分析。 先给大家讲个笑话乐呵一下: -有一天阿东到图书馆借了 N 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警器下过一下,以找出引发警报的书,但是保安露出不屑的眼神:你连二分查找都不会吗?于是保安把书分成两堆,让第一堆过一下报警器,报警器响;于是再把这堆书分成两堆…… 最终,检测了 logN 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是阿东背着剩下的书走了。 +有一天阿东到图书馆借了 `N` 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警器下过一下,以找出引发警报的书,但是保安露出不屑的眼神:你连二分查找都不会吗? -从此,图书馆丢了 N - 1 本书。 +于是保安把书分成两堆,让第一堆过一下报警器,报警器响,这说明引起报警的书包含在里面;于是再把这堆书分成两堆,把第一堆过一下报警器,报警器又响,继续分成两堆…… -二分查找真的很简单吗?并不简单。看看 Knuth 大佬(发明 KMP 算法的那位)怎么说的: +最终,检测了 `logN` 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是阿东背着剩下的书走了。 -Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky..、 +从此,图书馆丢了 `N - 1` 本书(手动狗头)。 -这句话可以这样理解:**思路很简单,细节是魔鬼。** -本文就来探究几个最常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。而且,我们就是要深入细节,比如不等号是否应该带等号,mid 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因,保证你能灵活准确地写出正确的二分查找算法。 -### 零、二分查找框架 + + + + +二分查找并不简单,Knuth 大佬(发明 KMP 算法的那位)都说二分查找:**思路很简单,细节是魔鬼**。很多人喜欢拿整型溢出的 bug 说事儿,但是二分查找真正的坑根本就不是那个细节问题,而是在于到底要给 `mid` 加一还是减一,while 里到底用 `<=` 还是 `<`。 + +你要是没有正确理解这些细节,写二分肯定就是玄学编程,有没有 bug 只能靠菩萨保佑,谁写谁知道。 + +本文就来探究几个最常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。而且,我们就是要深入细节,比如不等号是否应该带等号,`mid` 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因,保证你能灵活准确地写出正确的二分查找算法。 + +另外再声明一下,对于二分搜索的每一个场景,本文还会探讨多种代码写法,目的是为了让你理解出现这些细微差异的本质原因,最起码你看到别人的代码时不会懵逼。实际上这些写法没有优劣之分,你喜欢哪种就用哪种好了。 + +## 零、二分查找框架 ```java int binarySearch(int[] nums, int target) { int left = 0, right = ...; while(...) { - int mid = (right + left) / 2; + int mid = left + (right - left) / 2; if (nums[mid] == target) { ... } else if (nums[mid] < target) { @@ -38,56 +79,99 @@ int binarySearch(int[] nums, int target) { 其中 `...` 标记的部分,就是可能出现细节问题的地方,当你见到一个二分查找的代码时,首先注意这几个地方。后文用实例分析这些地方能有什么样的变化。 -另外声明一下,计算 mid 时需要技巧防止溢出,可以「参见前文」,本文暂时忽略这个问题。 +**另外提前说明一下,计算 `mid` 时需要防止溢出**,代码中 `left + (right - left) / 2` 就和 `(left + right) / 2` 的结果相同,但是有效防止了 `left` 和 `right` 太大,直接相加导致溢出的情况。 -### 一、寻找一个数(基本的二分搜索) -这个场景是最简单的,肯能也是大家最熟悉的,即搜索一个数,如果存在,返回其索引,否则返回 -1。 + + + + +## 一、寻找一个数(基本的二分搜索) + +这个场景是最简单的,可能也是大家最熟悉的,即搜索一个数,如果存在,返回其索引,否则返回 -1。 ```java -int binarySearch(int[] nums, int target) { - int left = 0; - int right = nums.length - 1; // 注意 - - while(left <= right) { - int mid = (right + left) / 2; - if(nums[mid] == target) - return mid; - else if (nums[mid] < target) - left = mid + 1; // 注意 - else if (nums[mid] > target) - right = mid - 1; // 注意 +class Solution { + // 标准的二分搜索框架,搜索目标元素的索引,若不存在则返回 -1 + public int search(int[] nums, int target) { + int left = 0; + // 注意 + int right = nums.length - 1; + + while(left <= right) { + int mid = left + (right - left) / 2; + if(nums[mid] == target) { + return mid; + } else if (nums[mid] < target) { + // 注意 + left = mid + 1; + } else if (nums[mid] > target) { + // 注意 + right = mid - 1; + } } - return -1; + return -1; + } } ``` -1、为什么 while 循环的条件中是 <=,而不是 < ? -答:因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。 +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
-这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。 -我们这个算法中使用的是前者 [left, right] 两端都闭的区间。**这个区间其实就是每次进行搜索的区间**。 + +这段代码可以解决力扣第 704 题「二分查找」,但我们深入探讨一下其中的细节。 + + + + + + + +### 为什么 while 循环的条件是 `<=` 而不是 `<`? + +答:因为初始化 `right` 的赋值是 `nums.length - 1`,即最后一个元素的索引,而不是 `nums.length`。 + +这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 `[left, right]`,后者相当于左闭右开区间 `[left, right)`。因为索引大小为 `nums.length` 是越界的,所以我们把 `right` 这一边视为开区间。 + +我们这个算法中使用的是前者 `[left, right]` 两端都闭的区间。**这个区间其实就是每次进行搜索的区间**。 什么时候应该停止搜索呢?当然,找到了目标值的时候可以终止: + + + + ```java if(nums[mid] == target) return mid; ``` + + 但如果没找到,就需要 while 循环终止,然后返回 -1。那 while 循环什么时候应该终止?**搜索区间为空的时候应该终止**,意味着你没得找了,就等于没找到嘛。 -`while(left <= right)` 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见**这时候区间为空**,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。 +`while(left <= right)` 的终止条件是 `left == right + 1`,写成区间的形式就是 `[right + 1, right]`,或者带个具体的数字进去 `[3, 2]`,可见**这时候区间为空**,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。 + +`while(left < right)` 的终止条件是 `left == right`,写成区间的形式就是 `[right, right]`,或者带个具体的数字进去 `[2, 2]`,**这时候区间非空**,还有一个数 2,但此时 while 循环终止了。也就是说区间 `[2, 2]` 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。 + +当然,如果你非要用 `while(left < right)` 也可以,我们已经知道了出错的原因,就打个补丁好了: + + -`while(left < right)` 的终止条件是 left == right,写成区间的形式就是 [left, right],或者带个具体的数字进去 [2, 2],**这时候区间非空**,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。 -当然,如果你非要用 while(left < right) 也可以,我们已经知道了出错的原因,就打个补丁好了: ```java - //... + // ... while(left < right) { // ... } @@ -95,170 +179,440 @@ int binarySearch(int[] nums, int target) { ``` -2、为什么 left = mid + 1,right = mid - 1?我看有的代码是 right = mid 或者 left = mid,没有这些加加减减,到底怎么回事,怎么判断? + +### 为什么是 `left = mid + 1`,`right = mid - 1`? + +为什么 `left = mid + 1`,`right = mid - 1`?我看有的代码是 `right = mid` 或者 `left = mid`,没有这些加加减减,到底怎么回事,怎么判断? 答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。 -刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,如何确定下一步的搜索区间呢? +刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 `[left, right]`。那么当我们发现索引 `mid` 不是要找的 `target` 时,下一步应该去搜索哪里呢? -当然是 [left, mid - 1] 或者 [mid + 1, right] 对不对?因为 mid 已经搜索过,应该从搜索区间中去除。 +当然是去搜索区间 `[left, mid-1]` 或者区间 `[mid+1, right]` 对不对?**因为 `mid` 已经搜索过,应该从搜索区间中去除**。 -3、此算法有什么缺陷? + + + + + + +### 此算法有什么缺陷? 答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。 -比如说给你有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。 +比如说给你有序数组 `nums = [1,2,2,2,3]`,`target` 为 2,此算法返回的索引是 2,没错。但是如果我想得到 `target` 的左侧边界,即索引 1,或者我想得到 `target` 的右侧边界,即索引 3,这样的话此算法是无法处理的。 -这样的需求很常见。你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。 +这样的需求很常见,**你也许会说,找到一个 `target`,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了**。 我们后续的算法就来讨论这两种二分查找的算法。 +## 二、寻找左侧边界的二分搜索 -### 二、寻找左侧边界的二分搜索 - -直接看代码,其中的标记是需要注意的细节: +以下是最常见的代码形式,其中的标记是需要注意的细节: ```java int left_bound(int[] nums, int target) { - if (nums.length == 0) return -1; int left = 0; - int right = nums.length; // 注意 + // 注意 + int right = nums.length; - while (left < right) { // 注意 - int mid = (left + right) / 2; + // 注意 + while (left < right) { + int mid = left + (right - left) / 2; if (nums[mid] == target) { right = mid; } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { - right = mid; // 注意 + // 注意 + right = mid; } } return left; } ``` -1、为什么 while(left < right) 而不是 <= ? +### 为什么 while 中是 `<` 而不是 `<=`? + +答:用相同的方法分析,因为 `right = nums.length` 而不是 `nums.length - 1`。因此每次循环的「搜索区间」是 `[left, right)` 左闭右开。 + +`while(left < right)` 终止的条件是 `left == right`,此时搜索区间 `[left, left)` 为空,所以可以正确终止。 + +> [!NOTE] +> 这里先要说一个搜索左右边界和上面这个算法的一个区别,也是很多读者问的:**刚才的 `right` 不是 `nums.length - 1` 吗,为啥这里非要写成 `nums.length` 使得「搜索区间」变成左闭右开呢**? +> +> 因为对于搜索左右侧边界的二分查找,这种写法比较普遍,我就拿这种写法举例了,保证你以后遇到这类代码可以理解。你非要用两端都闭的写法反而更简单,我会在后面写相关的代码,把三种二分搜索都用一种两端都闭的写法统一起来,你耐心往后看就行了。 + +### `target` 不存在时返回什么? + +如果 `nums` 中不存在 `target` 这个值,计算出来的这个索引含义是什么?如果我想让它返回 -1,怎么做? + + + + + + + +::: important 当 `target` 不存在时,`left_bound` 返回值的含义 + +这是一个很好且很重要的问题,你把这个地方理解了,在二分搜索的实际应用场景中就不会懵逼。 + +直接说结论:**如果 `target` 不存在,搜索左侧边界的二分搜索返回的索引是大于 `target` 的最小索引**。 -答:用相同的方法分析,因为 right = nums.length 而不是 nums.length - 1 。因此每次循环的「搜索区间」是 [left, right) 左闭右开。 +举个例子,`nums = [2,3,5,7], target = 4`,`left_bound` 函数返回值是 2,因为元素 5 是大于 4 的最小元素。 -while(left < right) 终止的条件是 left == right,此时搜索区间 [left, left) 为空,所以可以正确终止。 +有点绕晕了是吧?这个 `left_bound` 函数明明是搜索左边界的,但是当 `target` 不存在的时候,却返回的是大于 `target` 的最小索引。这个结论不用死记,你要是拿不准,简单举个例子就能得到这个结论了。 -2、为什么没有返回 -1 的操作?如果 nums 中不存在 target 这个值,怎么办? +所以跟你说二分搜索这个东西思路很简单,细节是魔鬼嘛,里面的坑太多了。要是真想考你,总有办法可以把你考到怀疑人生。 -答:因为要一步一步来,先理解一下这个「左侧边界」有什么特殊含义: +不是我故意把代码模板总结的这么复杂,而是二分搜索本身就很复杂,这些细节是不可能绕开的,如果你之前没有了解过这些细节,只能说明你之前学得不扎实。就算不用我总结的模板,你也必须搞清楚当 `target` 不存在时算法的行为,否则出了 bug 你都不知道咋改,真有这么严重。 -![](../pictures/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/binarySearch1.png) +话说回来,`left_bound` 的这个行为有一个好处。比方说现在让你写一个 `floor` 函数,就可以直接用 `left_bound` 函数来实现: + + + + + +```java +// 在一个有序数组中,找到「小于 target 的最大元素的索引」 +// 比如说输入 nums = [1,2,2,2,3],target = 2,函数返回 0,因为 1 是小于 2 的最大元素。 +// 再比如输入 nums = [1,2,3,5,6],target = 4,函数返回 2,因为 3 是小于 4 的最大元素。 +int floor(int[] nums, int target) { + // 当 target 不存在,比如输入 [4,6,8,10], target = 7 + // left_bound 返回 2,减一就是 1,元素 6 就是小于 7 的最大元素 + // 当 target 存在,比如输入 [4,6,8,8,8,10], target = 8 + // left_bound 返回 2,减一就是 1,元素 6 就是小于 8 的最大元素 + return left_bound(nums, target) - 1; +} +``` + + + +最后,我的建议是,如果你必须手写二分代码,那么你一定要了解清楚代码的种种行为,本文总结的框架就是在帮你理清这里面的细节。如果非必要,不要自己手写,尽肯能用编程语言提供的标准库函数,可以节约时间,而且标准库函数的行为在文档里都有明确的说明,不容易出错。 + +::: + +如果想让 `target` 不存在时返回 -1 其实很简单,在返回的时候额外判断一下 `nums[left]` 是否等于 `target` 就行了,如果不等于,就说明 `target` 不存在。需要注意的是,访问数组索引之前要保证索引不越界: -对于这个数组,算法会返回 1。这个 1 的含义可以这样解读:nums 中小于 2 的元素有 1 个。 -比如对于有序数组 nums = [2,3,5,7], target = 1,算法会返回 0,含义是:nums 中小于 1 的元素有 0 个。 -再比如说 nums 不变,target = 8,算法会返回 4,含义是:nums 中小于 8 的元素有 4 个。 -综上可以看出,函数的返回值(即 left 变量的值)取值区间是闭区间 [0, nums.length],所以我们简单添加两行代码就能在正确的时候 return -1: ```java while (left < right) { - //... + // ... } -// target 比所有数都大 -if (left == nums.length) return -1; -// 类似之前算法的处理方式 +// 如果索引越界,说明数组中无目标元素,返回 -1 +if (left < 0 || left >= nums.length) { + return -1; +} +// 提示:其实上面的 if 中 left < 0 这个判断可以省略,因为对于这个算法,left 不可能小于 0 +// 你看这个算法执行的逻辑,left 初始化就是 0,且只可能一直往右走,那么只可能在右侧越界 +// 不过我这里就同时判断了,因为在访问数组索引之前保证索引在左右两端都不越界是一个好习惯,没有坏处 +// 另一个好处是让二分的模板更统一,降低你的记忆成本,因为等会儿寻找右边界的时候也有类似的出界判断 + +// 判断一下 nums[left] 是不是 target return nums[left] == target ? left : -1; ``` -1、为什么 left = mid + 1,right = mid ?和之前的算法不一样? -答:这个很好解释,因为我们的「搜索区间」是 [left, right) 左闭右开,所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid 分割成两个区间,即 [left, mid) 或 [mid + 1, right)。 -4、为什么该算法能够搜索左侧边界? +### 为什么是 `left = mid + 1` 和 `right = mid`? + +为什么 `left = mid + 1`,`right = mid` ?和之前的算法不一样? -答:关键在于对于 nums[mid] == target 这种情况的处理: +答:这个很好解释,因为我们的「搜索区间」是 `[left, right)` 左闭右开,所以当 `nums[mid]` 被检测之后,下一步应该去 `mid` 的左侧或者右侧区间搜索,即 `[left, mid)` 或 `[mid + 1, right)`。 + +### 为什么该算法能够搜索左侧边界? + +答:关键在于对于 `nums[mid] == target` 这种情况的处理: ```java if (nums[mid] == target) right = mid; ``` -可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。 +可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 `right`,在区间 `[left, mid)` 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。 + + + + + + + +### 为什么返回 `left` 而不是 `right`? + +答:都是一样的,因为 while 终止的条件是 `left == right`。 + +### 能否统一成两端都闭的搜索区间? + +能不能想办法把 `right` 变成 `nums.length - 1`,也就是继续使用两边都闭的「搜索区间」?这样就可以和第一种二分搜索在某种程度上统一起来了。 + +答:当然可以,只要你明白了「搜索区间」这个概念,就能有效避免漏掉元素,随便你怎么改都行。下面我们严格根据逻辑来修改: + +因为你非要让搜索区间两端都闭,所以 `right` 应该初始化为 `nums.length - 1`,while 的终止条件应该是 `left == right + 1`,也就是其中应该用 `<=`: + +```java +int left_bound(int[] nums, int target) { + // 搜索区间为 [left, right] + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + // if else ... + } +} +``` + +因为搜索区间是两端都闭的,且现在是搜索左侧边界,所以 `left` 和 `right` 的更新逻辑如下: + +```java +if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1; +} else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1; +} else if (nums[mid] == target) { + // 收缩右侧边界 + right = mid - 1; +} +``` + +和刚才相同,如果想在找不到 `target` 的时候返回 -1,那么检查一下 `nums[left]` 和 `target` 是否相等即可: + +```java +// 此时 target 比所有数都大,返回 -1 +if (left == nums.length) return -1; +// 判断一下 nums[left] 是不是 target +return nums[left] == target ? left : -1; +``` + +至此,整个算法就写完了,完整代码如下: + +```java +int left_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + // 搜索区间为 [left, right] + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + // 搜索区间变为 [mid+1, right] + left = mid + 1; + } else if (nums[mid] > target) { + // 搜索区间变为 [left, mid-1] + right = mid - 1; + } else if (nums[mid] == target) { + // 收缩右侧边界 + right = mid - 1; + } + } + // 判断 target 是否存在于 nums 中 + // 如果越界,target 肯定不存在,返回 -1 + if (left < 0 || left >= nums.length) { + return -1; + } + // 判断一下 nums[left] 是不是 target + return nums[left] == target ? left : -1; +} +``` + + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
-5、为什么返回 left 而不是 right? -答:都是一样的,因为 while 终止的条件是 left == right。 +这样就和第一种二分搜索算法统一了,都是两端都闭的「搜索区间」,而且最后返回的也是 `left` 变量的值。只要把住二分搜索的逻辑,两种形式大家看自己喜欢哪种记哪种吧。 -### 三、寻找右侧边界的二分查找 +## 三、寻找右侧边界的二分查找 -寻找右侧边界和寻找左侧边界的代码差不多,只有两处不同,已标注: +类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同: ```java int right_bound(int[] nums, int target) { - if (nums.length == 0) return -1; int left = 0, right = nums.length; while (left < right) { - int mid = (left + right) / 2; + int mid = left + (right - left) / 2; if (nums[mid] == target) { - left = mid + 1; // 注意 + // 注意 + left = mid + 1; } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; } } - return left - 1; // 注意 + // 注意 + return left - 1; } ``` -1、为什么这个算法能够找到右侧边界? +### 为什么这个算法能够找到右侧边界? 答:类似地,关键点还是这里: ```java - if (nums[mid] == target) { - left = mid + 1; +if (nums[mid] == target) { + left = mid + 1; +} ``` -当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。 +当 `nums[mid] == target` 时,不要立即返回,而是增大「搜索区间」的左边界 `left`,使得区间不断向右靠拢,达到锁定右侧边界的目的。 + + + + + + + +### 为什么返回 `left - 1`? + +为什么最后返回 `left - 1` 而不像左侧边界的函数,返回 `left`?而且我觉得这里既然是搜索右侧边界,应该返回 `right` 才对。 + +答:首先,while 循环的终止条件是 `left == right`,所以 `left` 和 `right` 是一样的,你非要体现右侧的特点,返回 `right - 1` 好了。 + +至于为什么要减一,这是搜索右侧边界的一个特殊点,关键在锁定右边界时的这个条件判断: + -2、为什么最后返回 left - 1 而不像左侧边界的函数,返回 left?而且我觉得这里既然是搜索右侧边界,应该返回 right 才对。 -答:首先,while 循环的终止条件是 left == right,所以 left 和 right 是一样的,你非要体现右侧的特点,返回 right - 1 好了。 -至于为什么要减一,这是搜索右侧边界的一个特殊点,关键在这个条件判断: ```java - if (nums[mid] == target) { - left = mid + 1; - // 这样想: mid = left - 1 +// 增大 left,锁定右侧边界 +if (nums[mid] == target) { + left = mid + 1; + // 这样想: mid = left - 1 +} ``` -![](../pictures/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/binarySearch2.png) +![](https://labuladong.online/algo/images/binary-search/3.jpg) -因为我们对 left 的更新必须是 left = mid + 1,就是说 while 循环结束时,nums[left] 一定不等于 target 了,而 nums[left-1] 可能是 target。 -至于为什么 left 的更新必须是 left = mid + 1,同左侧边界搜索,就不再赘述。 -1、为什么没有返回 -1 的操作?如果 nums 中不存在 target 这个值,怎么办? +因为我们对 `left` 的更新必须是 `left = mid + 1`,就是说 while 循环结束时,`nums[left]` 一定不等于 `target` 了,而 `nums[left-1]` 可能是 `target`。 -答:类似之前的左侧边界搜索,因为 while 的终止条件是 left == right,就是说 left 的取值范围是 [0, nums.length],所以可以添加两行代码,正确地返回 -1: +至于为什么 `left` 的更新必须是 `left = mid + 1`,当然是为了把 `nums[mid]` 排除出搜索区间,这里就不再赘述。 + +### 如果 `target` 不存在时返回什么? + +如果 `nums` 中不存在 `target` 这个值,计算出来的这个索引含义是什么?如果我想让它返回 -1,怎么做? + +::: important 当 `target` 不存在时,`right_bound` 返回值的含义 + +直接说结论,和前面讲的 `left_bound` 相反:**如果 `target` 不存在,搜索右侧边界的二分搜索返回的索引是小于 `target` 的最大索引**。 + +这个结论不用死记,你要是拿不准,简单举个例子就能得到这个结论了。比如 `nums = [2,3,5,7], target = 4`,`right_bound` 函数返回值是 1,因为元素 3 是小于 4 的最大元素。 + +与前面的建议相同,考虑到二分搜索代码细节的复杂性,如果非必要,不要自己手写,尽肯能用编程语言提供的标准库函数。 + +::: + + +如果你想在 `target` 不存在时返回 -1,很简单,只要在最后判断一下 `nums[left-1]` 是不是 `target` 就行了,类似之前的左侧边界搜索,做一点额外的判断即可: ```java while (left < right) { // ... } -if (left == 0) return -1; -return nums[left-1] == target ? (left-1) : -1; +// 判断 target 是否存在于 nums 中 +// left - 1 索引越界的话 target 肯定不存在 +if (left - 1 < 0 || left - 1 >= nums.length) { + return -1; +} +// 判断一下 nums[left - 1] 是不是 target +return nums[left - 1] == target ? (left - 1) : -1; ``` -### 四、最后总结 +**4、是否也可以把这个算法的「搜索区间」也统一成两端都闭的形式呢?这样这三个写法就完全统一了,以后就可以闭着眼睛写出来了**。 -来梳理一下这些细节差异的因果逻辑: +答:当然可以,类似搜索左侧边界的统一写法,其实只要改两个地方就行了: + +```java +int right_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // 这里改成收缩左侧边界即可 + left = mid + 1; + } + } + // 最后改成返回 left - 1 + if (left - 1 < 0 || left - 1 >= nums.length) { + return -1; + } + return nums[left - 1] == target ? (left - 1) : -1; +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
-第一个,最基本的二分查找算法: -```python + +当然,由于 while 的结束条件为 `right == left - 1`,所以你把上述代码中的 `left - 1` 都改成 `right` 也没有问题,这样可能更有利于看出来这是在「搜索右侧边界」: + + + + + +```java +int right_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // 这里改成收缩左侧边界即可 + left = mid + 1; + } + } + // 最后改成返回 right + if (right < 0 || right >= nums.length) { + return -1; + } + return nums[right] == target ? right : -1; +} +``` + + + +至此,搜索右侧边界的二分查找的两种写法也完成了,其实将「搜索区间」统一成两端都闭反而更容易记忆,你说是吧? + +## 四、逻辑统一 + +有了搜索左右边界的二分搜索,你可以去解决力扣第 34 题「在排序数组中查找元素的第一个和最后一个位置」。接下来梳理一下这些细节差异的因果逻辑: + +**第一个,最基本的二分查找算法**: + + + + + +```java 因为我们初始化 right = nums.length - 1 所以决定了我们的「搜索区间」是 [left, right] 所以决定了 while (left <= right) @@ -268,9 +622,15 @@ return nums[left-1] == target ? (left-1) : -1; 所以当 nums[mid] == target 时可以立即返回 ``` -第二个,寻找左侧边界的二分查找: -```python + +**第二个,寻找左侧边界的二分查找**: + + + + + +```java 因为我们初始化 right = nums.length 所以决定了我们的「搜索区间」是 [left, right) 所以决定了 while (left < right) @@ -281,9 +641,15 @@ return nums[left-1] == target ? (left-1) : -1; 而要收紧右侧边界以锁定左侧边界 ``` -第三个,寻找右侧边界的二分查找: -```python + +**第三个,寻找右侧边界的二分查找**: + + + + + +```java 因为我们初始化 right = nums.length 所以决定了我们的「搜索区间」是 [left, right) 所以决定了 while (left < right) @@ -297,23 +663,92 @@ return nums[left-1] == target ? (left-1) : -1; 所以最后无论返回 left 还是 right,必须减一 ``` -如果以上内容你都能理解,那么恭喜你,二分查找算法的细节不过如此。 -通过本文,你学会了: -1、分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。 +对于寻找左右边界的二分搜索,比较常见的手法是使用左闭右开的「搜索区间」,**我们还根据逻辑将「搜索区间」全都统一成了两端都闭,便于记忆,只要修改两处即可变化出三种写法**: + + + + + + + +如果以上内容你都能理解,那么恭喜你,二分查找算法的细节不过如此。通过本文,你学会了: + +1、分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。把逻辑写对之后再合并分支,提升运行效率。 2、注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。 -3、如需要搜索左右边界,只要在 nums[mid] == target 时做修改即可。搜索右侧时需要减一。 +3、如需定义左闭右开的「搜索区间」搜索左右边界,只要在 `nums[mid] == target` 时做修改即可,搜索右侧时需要减一。 + +4、如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 `nums[mid] == target` 条件处的代码和返回的逻辑即可,推荐拿小本本记下,作为二分搜索模板。 + +最后我想说,以上二分搜索的框架属于「术」的范畴,如果上升到「道」的层面,**二分思维的精髓就是:通过已知信息尽可能多地收缩(折半)搜索空间**,从而增加穷举效率,快速找到目标。 + +理解本文能保证你写出正确的二分查找的代码,但实际题目中不会直接让你写二分代码,我会在 [二分查找的运用](https://labuladong.online/algo/frequency-interview/binary-search-in-action/) 和 [二分查找的更多习题](https://labuladong.online/algo/problem-set/binary-search/) 中进一步讲解如何把二分思维运用到更多算法题中。 + + + + + + + +
+
+引用本文的文章 + + - [base case 和备忘录的初始值怎么定?](https://labuladong.online/algo/dynamic-programming/memo-fundamental/) + - [【强化练习】二分搜索算法经典习题](https://labuladong.online/algo/problem-set/binary-search/) + - [一文秒杀所有丑数系列问题](https://labuladong.online/algo/frequency-interview/ugly-number-summary/) + - [动态规划设计:最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) + - [双指针技巧秒杀七道数组题目](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [实际运用二分搜索时的思维框架](https://labuladong.online/algo/frequency-interview/binary-search-in-action/) + - [带权重的随机选择算法](https://labuladong.online/algo/frequency-interview/random-pick-with-weight/) + - [拓展:快速排序详解及应用](https://labuladong.online/algo/practice-in-action/quick-sort/) + - [浅谈存储系统:LSM 树设计原理](https://labuladong.online/algo/fname.html?fname=LSM树) + - [用算法打败算法](https://labuladong.online/algo/fname.html?fname=PDF中的算法) + - [算法刷题的重点和坑](https://labuladong.online/algo/intro/how-to-learn-algorithms/) + - [讲两道常考的阶乘算法题](https://labuladong.online/algo/frequency-interview/factorial-problems/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1201. Ugly Number III](https://leetcode.com/problems/ugly-number-iii/?show=1) | [1201. 丑数 III](https://leetcode.cn/problems/ugly-number-iii/?show=1) | 🟠 | +| [1235. Maximum Profit in Job Scheduling](https://leetcode.com/problems/maximum-profit-in-job-scheduling/?show=1) | [1235. 规划兼职工作](https://leetcode.cn/problems/maximum-profit-in-job-scheduling/?show=1) | 🔴 | +| [162. Find Peak Element](https://leetcode.com/problems/find-peak-element/?show=1) | [162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/?show=1) | 🟠 | +| [240. Search a 2D Matrix II](https://leetcode.com/problems/search-a-2d-matrix-ii/?show=1) | [240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/?show=1) | 🟠 | +| [33. Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array/?show=1) | [33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/?show=1) | 🟠 | +| [35. Search Insert Position](https://leetcode.com/problems/search-insert-position/?show=1) | [35. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/?show=1) | 🟢 | +| [658. Find K Closest Elements](https://leetcode.com/problems/find-k-closest-elements/?show=1) | [658. 找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/?show=1) | 🟠 | +| [74. Search a 2D Matrix](https://leetcode.com/problems/search-a-2d-matrix/?show=1) | [74. 搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/?show=1) | 🟠 | +| [792. Number of Matching Subsequences](https://leetcode.com/problems/number-of-matching-subsequences/?show=1) | [792. 匹配子序列的单词数](https://leetcode.cn/problems/number-of-matching-subsequences/?show=1) | 🟠 | +| [793. Preimage Size of Factorial Zeroes Function](https://leetcode.com/problems/preimage-size-of-factorial-zeroes-function/?show=1) | [793. 阶乘函数后 K 个零](https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function/?show=1) | 🔴 | +| [81. Search in Rotated Sorted Array II](https://leetcode.com/problems/search-in-rotated-sorted-array-ii/?show=1) | [81. 搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/?show=1) | 🟠 | +| [852. Peak Index in a Mountain Array](https://leetcode.com/problems/peak-index-in-a-mountain-array/?show=1) | [852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/?show=1) | 🟠 | +| - | [剑指 Offer 04. 二维数组中的查找](https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 53 - I. 在排序数组中查找数字 I](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 53 - II. 0~n-1中缺失的数字](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/?show=1) | 🟢 | +| - | [剑指 Offer II 068. 查找插入位置](https://leetcode.cn/problems/N6YdxV/?show=1) | 🟢 | +| - | [剑指 Offer II 069. 山峰数组的顶部](https://leetcode.cn/problems/B1IidL/?show=1) | 🟢 | + +
+
-呵呵,此文对二分查找的问题无敌好吧!**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:回溯算法解题框架](../算法思维系列/回溯算法详解修订版.md) -[下一篇:滑动窗口解题框架](../算法思维系列/滑动窗口技巧.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" deleted file mode 100644 index 4f5e678b40..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" +++ /dev/null @@ -1,117 +0,0 @@ -# 信封嵌套问题 - -很多算法问题都需要排序技巧,其难点不在于排序本身,而是需要巧妙地排序进行预处理,将算法问题进行转换,为之后的操作打下基础。 - -信封嵌套问题就需要先按特定的规则排序,之后就转换为一个 [最长递增子序列问题](../动态规划系列/动态规划设计:最长递增子序列.md),可以用前文 [二分查找详解](二分查找详解.md) 的技巧来解决了。 - -### 一、题目概述 - -信封嵌套问题是个很有意思且经常出现在生活中的问题,先看下题目: - -![title](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/title.png) - -这道题目其实是最长递增子序列(Longes Increasing Subsequence,简写为 LIS)的一个变种,因为很显然,每次合法的嵌套是大的套小的,相当于找一个最长递增的子序列,其长度就是最多能嵌套的信封个数。 - -但是难点在于,标准的 LIS 算法只能在数组中寻找最长子序列,而我们的信封是由 `(w, h)` 这样的二维数对形式表示的,如何把 LIS 算法运用过来呢? - -![0](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/0.jpg) - -读者也许会想,通过 `w × h` 计算面积,然后对面积进行标准的 LIS 算法。但是稍加思考就会发现这样不行,比如 `1 × 10` 大于 `3 × 3`,但是显然这样的两个信封是无法互相嵌套的。 - -### 二、解法 - -这道题的解法是比较巧妙的: - -**先对宽度 `w` 进行升序排序,如果遇到 `w` 相同的情况,则按照高度 `h` 降序排序。之后把所有的 `h` 作为一个数组,在这个数组上计算 LIS 的长度就是答案。** - -画个图理解一下,先对这些数对进行排序: - -![1](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/1.jpg) - -然后在 `h` 上寻找最长递增子序列: - -![2](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/2.jpg) - -这个子序列就是最优的嵌套方案。 - -这个解法的关键在于,对于宽度 `w` 相同的数对,要对其高度 `h` 进行降序排序。因为两个宽度相同的信封不能相互包含的,逆序排序保证在 `w` 相同的数对中最多只选取一个。 - -下面看代码: - -```java -// envelopes = [[w, h], [w, h]...] -public int maxEnvelopes(int[][] envelopes) { - int n = envelopes.length; - // 按宽度升序排列,如果宽度一样,则按高度降序排列 - Arrays.sort(envelopes, new Comparator() - { - public int compare(int[] a, int[] b) { - return a[0] == b[0] ? - b[1] - a[1] : a[0] - b[0]; - } - }); - // 对高度数组寻找 LIS - int[] height = new int[n]; - for (int i = 0; i < n; i++) - height[i] = envelopes[i][1]; - - return lengthOfLIS(height); -} -``` - -关于最长递增子序列的寻找方法,在前文中详细介绍了动态规划解法,并用扑克牌游戏解释了二分查找解法,本文就不展开了,直接套用算法模板: - -```java -/* 返回 nums 中 LIS 的长度 */ -public int lengthOfLIS(int[] nums) { - int piles = 0, n = nums.length; - int[] top = new int[n]; - for (int i = 0; i < n; i++) { - // 要处理的扑克牌 - int poker = nums[i]; - int left = 0, right = piles; - // 二分查找插入位置 - while (left < right) { - int mid = (left + right) / 2; - if (top[mid] >= poker) - right = mid; - else - left = mid + 1; - } - if (left == piles) piles++; - // 把这张牌放到牌堆顶 - top[left] = poker; - } - // 牌堆数就是 LIS 长度 - return piles; -} -``` - -为了清晰,我将代码分为了两个函数, 你也可以合并,这样可以节省下 `height` 数组的空间。 - -此算法的时间复杂度为 $O(NlogN)$,因为排序和计算 LIS 各需要 $O(NlogN)$ 的时间。 - -空间复杂度为 $O(N)$,因为计算 LIS 的函数中需要一个 `top` 数组。 - -### 三、总结 - -这个问题是个 Hard 级别的题目,难就难在排序,正确地排序后此问题就被转化成了一个标准的 LIS 问题,容易解决一些。 - -其实这种问题还可以拓展到三维,比如说现在不是让你嵌套信封,而是嵌套箱子,每个箱子有长宽高三个维度,请你算算最多能嵌套几个箱子? - -我们可能会这样想,先把前两个维度(长和宽)按信封嵌套的思路求一个嵌套序列,最后在这个序列的第三个维度(高度)找一下 LIS,应该能算出答案。 - -实际上,这个思路是错误的。这类问题叫做「偏序问题」,上升到三维会使难度巨幅提升,需要借助一种高级数据结构「树状数组」,有兴趣的读者可以自行搜索。 - -有很多算法问题都需要排序后进行处理,阿东正在进行整理总结。希望本文对你有帮助。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:区间调度之区间交集问题](../算法思维系列/区间交集问题.md) - -[下一篇:几个反直觉的概率问题](../算法思维系列/几个反直觉的概率问题.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" index 33e5426f32..7388c9d0da 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" @@ -1,6 +1,18 @@ # 几个反直觉的概率问题 -上篇文章 [洗牌算法详解](./洗牌算法.md) 讲到了验证概率算法的蒙特卡罗方法,今天聊点轻松的内容:几个和概率相关的有趣问题。 + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + + + +上篇文章 [谈谈游戏中的随机算法](https://labuladong.online/algo/frequency-interview/random-algorithm/) 讲到了验证概率算法的蒙特卡罗方法,今天聊点轻松的内容:几个和概率相关的有趣问题。 计算概率有下面两个最简单的原则: @@ -15,6 +27,11 @@ 下面介绍几个简单却具有迷惑性的问题,分别是男孩女孩问题、生日悖论、三门问题。当然,三门问题可能是大家最耳熟的,所以就多说一些有趣的思考。 + + + + + ### 一、男孩女孩问题 假设有一个家庭,有两个孩子,现在告诉你其中有一个男孩,请问另一个也是男孩的概率是多少? @@ -38,13 +55,18 @@ 我竟然觉得有那么一丝道理!但其实,我们只是通过年龄差异来表示两个孩子的独立性,也就是说即便两个孩子同性,也有两种可能。所以不要用双胞胎抬杠了。 + + + + + ### 二、生日悖论 生日悖论是由这样一个问题引出的:一个屋子里需要有多少人,才能使得存在至少两个人生日是同一天的概率达到 50%? 答案是 23 个人,也就是说房子里如果有 23 个人,那么就有 50% 的概率会存在两个人生日相同。这个结论看起来不可思议,所以被称为悖论。按照直觉,要得到 50% 的概率,起码得有 183 个人吧,因为一年有 365 天呀?其实不是的,觉得这个结论不可思议主要有两个思维误区: -**第一个误区是误解「存在」这个词的含义。** +**第一个误区是误解「存在」这个词的含义**。 读者可能认为,如果 23 个人中出现相同生日的概率就能达到 50%,是不是意味着: @@ -58,22 +80,28 @@ 这样计算得到的结果是不是看起来合理多了?生日悖论计算对象的不是某一个人,而是一个整体,其中包含了所有人的排列组合,它们的概率之和当然会大得多。 -**第二个误区是认为概率是线性变化的。** +**第二个误区是认为概率是线性变化的**。 读者可能认为,如果 23 个人中出现相同生日的概率就能达到 50%,是不是意味着 46 个人的概率就能达到 100%? 不是的,就像中奖率 50% 的游戏,你玩两次的中奖率就是 100% 吗?显然不是,你玩两次的中奖率是 75%: -$P(两次能中奖) = P(第一次就中了) + P(第一次没中但第二次中了) = 1/2 + 1/2*1/2 = 75\%$ +`P(两次能中奖) = P(第一次就中了) + P(第一次没中但第二次中了) = 1/2 + 1/2*1/2 = 75%` 那么换到生日悖论也是一个道理,概率不是简单叠加,而要考虑一个连续的过程,所以这个结论并没有什么不合常理之处。 -那为什么只要 23 个人出现相同生日的概率就能大于 50% 了呢?我们先计算 23 个人生日都唯一(不重复)的概率。只有 1 个人的时候,生日唯一的概率是 $365/365$,2 个人时,生日唯一的概率是 $365/365 × 364/365$,以此类推可知 23 人的生日都唯一的概率: +那为什么只要 23 个人出现相同生日的概率就能大于 50% 了呢?我们先计算 23 个人生日都唯一(不重复)的概率。只有 1 个人的时候,生日唯一的概率是 `365/365`,2 个人时,生日唯一的概率是 `365/365 × 364/365`,以此类推可知 23 人的生日都唯一的概率: -![](../pictures/概率问题/p.png) +![](https://labuladong.online/algo/images/probability/p.png) 算出来大约是 0.493,所以存在相同生日的概率就是 0.507,差不多就是 50% 了。实际上,按照这个算法,当人数达到 70 时,存在两个人生日相同的概率就上升到了 99.9%,基本可以认为是 100% 了。所以从概率上说,一个几十人的小团体中存在生日相同的人真没啥稀奇的。 + + + + + + ### 三、三门问题 这个游戏很经典了:游戏参与者面对三扇门,其中两扇门后面是山羊,一扇门后面是跑车。参与者只要随便选一扇门,门后面的东西就归他(跑车的价值当然更大)。但是主持人决定帮一下参与者:在他选择之后,先不急着打开这扇门,而是由主持人打开剩下两扇门中的一扇,展示其中的山羊(主持人知道每扇门后面是什么),然后给参与者一次换门的机会,此时参与者应该换门还是不换门呢? @@ -82,13 +110,13 @@ $P(两次能中奖) = P(第一次就中了) + P(第一次没中但第二次中 你是游戏参与者,现在有门 1,2,3,假设你随机选择了门 1,然后主持人打开了门 3 告诉你那后面是山羊。现在,你是坚持你最初的选择门 1,还是选择换成门 2 呢? -![](../pictures/概率问题/sanmen.png) +![](https://labuladong.online/algo/images/probability/sanmen.png) 答案是应该换门,换门之后抽到跑车的概率是 2/3,不换的话是 1/3。又一次反直觉,感觉换不换的中奖概率应该都一样啊,因为最后肯定就剩两个门,一个是羊,一个是跑车,这是事实,所以不管选哪个的概率不都是 1/2 吗? 类似前面说的男孩女孩问题,最简单稳妥的方法就是把所有可能结果穷举出来: -![穷举树](../pictures/概率问题/tree.png) +![](https://labuladong.online/algo/images/probability/tree.png) 很容易看到选择换门中奖的概率是 2/3,不换的话是 1/3。 @@ -106,21 +134,16 @@ $P(两次能中奖) = P(第一次就中了) + P(第一次没中但第二次中 对于知情的你,你知道在二号箱子摸球概率大,所以只在二号箱摸,摸到红球的概率是:0 × 2/6 + 1 × 4/6 = 2/3 -三门问题是有指导意义的。比如你蒙选择题,先蒙了 A,后来灵机一动排除了 B 和 C,请问你是否要把 A 换成 D?答案是,换! -也许读者会问,如果只排除了一个答案,比如说 B,那么我是否应该把 A 换成 C 或者 D 呢?答案是,换! -因为按照刚才「浓缩」概率这个思想,只要进行了排除,都是在进行「浓缩」,均摊下来肯定比你一开始蒙的那个答案概率 1/4 高。比如刚才的例子,C 和 D 的正确概率都是 3/8,而你开始蒙的 A 只有 1/4。 -当然,运用此策略蒙题的前提是你真的抓瞎,真的随机乱选答案,这样概率才能作为最后的杀手锏。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:信封嵌套问题](../算法思维系列/信封嵌套问题.md) -[下一篇:洗牌算法](../算法思维系列/洗牌算法.md) +**_____________** + + -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" index 98960c02b3..d38bd06801 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" @@ -1,141 +1,251 @@ -# 前缀和技巧 +# 小而美的算法技巧:前缀和数组 -今天来聊一道简单却十分巧妙的算法问题:算出一共有几个和为 k 的子数组。 -![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/title.png) -那我把所有子数组都穷举出来,算它们的和,看看谁的和等于 k 不就行了。 +![](https://labuladong.online/algo/images/souyisou1.png) -关键是,**如何快速得到某个子数组的和呢**,比如说给你一个数组 `nums`,让你实现一个接口 `sum(i, j)`,这个接口要返回 `nums[i..j]` 的和,而且会被多次调用,你怎么实现这个接口呢? +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -因为接口要被多次调用,显然不能每次都去遍历 `nums[i..j]`,有没有一种快速的方法在 O(1) 时间内算出 `nums[i..j]` 呢?这就需要**前缀和**技巧了。 -### 一、什么是前缀和 -前缀和的思路是这样的,对于一个给定的数组 `nums`,我们额外开辟一个前缀和数组进行预处理: +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: -```java -int n = nums.length; -// 前缀和数组 -int[] preSum = new int[n + 1]; -preSum[0] = 0; -for (int i = 0; i < n; i++) - preSum[i + 1] = preSum[i] + nums[i]; -``` +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [303. Range Sum Query - Immutable](https://leetcode.com/problems/range-sum-query-immutable/) | [303. 区域和检索 - 数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) | 🟢 | +| [304. Range Sum Query 2D - Immutable](https://leetcode.com/problems/range-sum-query-2d-immutable/) | [304. 二维区域和检索 - 矩阵不可变](https://leetcode.cn/problems/range-sum-query-2d-immutable/) | 🟠 | -![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/1.jpg) +**-----------** -这个前缀和数组 `preSum` 的含义也很好理解,`preSum[i]` 就是 `nums[0..i-1]` 的和。那么如果我们想求 `nums[i..j]` 的和,只需要一步操作 `preSum[j+1]-preSum[i]` 即可,而不需要重新去遍历数组了。 -回到这个子数组问题,我们想求有多少个子数组的和为 k,借助前缀和技巧很容易写出一个解法: -```java -int subarraySum(int[] nums, int k) { - int n = nums.length; - // 构造前缀和 - int[] sum = new int[n + 1]; - sum[0] = 0; - for (int i = 0; i < n; i++) - sum[i + 1] = sum[i] + nums[i]; - - int ans = 0; - // 穷举所有子数组 - for (int i = 1; i <= n; i++) - for (int j = 0; j < i; j++) - // sum of nums[j..i-1] - if (sum[i] - sum[j] == k) - ans++; - - return ans; -} -``` +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) -这个解法的时间复杂度 $O(N^2)$ 空间复杂度 $O(N)$,并不是最优的解法。不过通过这个解法理解了前缀和数组的工作原理之后,可以使用一些巧妙的办法把时间复杂度进一步降低。 +前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。 -### 二、优化解法 +## 一维数组中的前缀和 + +先看一道例题,力扣第 303 题「区域和检索 - 数组不可变」,让你计算数组区间内元素的和,这是一道标准的前缀和问题: -前面的解法有嵌套的 for 循环: -```java -for (int i = 1; i <= n; i++) - for (int j = 0; j < i; j++) - if (sum[i] - sum[j] == k) - ans++; -``` -第二层 for 循环在干嘛呢?翻译一下就是,**在计算,有几个 `j` 能够使得 `sum[i]` 和 `sum[j]` 的差为 k。**毎找到一个这样的 `j`,就把结果加一。 -我们可以把 if 语句里的条件判断移项,这样写: + + ```java -if (sum[j] == sum[i] - k) - ans++; +// 题目要求你实现这样一个类 +class NumArray { + + public NumArray(int[] nums) {} + + // 查询闭区间 [left, right] 的累加和 + public int sumRange(int left, int right) {} +} ``` -优化的思路是:**我直接记录下有几个 `sum[j]` 和 `sum[i] - k` 相等,直接更新结果,就避免了内层的 for 循环**。我们可以用哈希表,在记录前缀和的同时记录该前缀和出现的次数。 +`sumRange` 函数需要计算并返回一个索引区间之内的元素和,没学过前缀和的人可能写出如下代码: ```java -int subarraySum(int[] nums, int k) { - int n = nums.length; - // map:前缀和 -> 该前缀和出现的次数 - HashMap - preSum = new HashMap<>(); - // base case - preSum.put(0, 1); - - int ans = 0, sum0_i = 0; - for (int i = 0; i < n; i++) { - sum0_i += nums[i]; - // 这是我们想找的前缀和 nums[0..j] - int sum0_j = sum0_i - k; - // 如果前面有这个前缀和,则直接更新答案 - if (preSum.containsKey(sum0_j)) - ans += preSum.get(sum0_j); - // 把前缀和 nums[0..i] 加入并记录出现次数 - preSum.put(sum0_i, - preSum.getOrDefault(sum0_i, 0) + 1); +class NumArray { + // 前缀和数组 + private int[] preSum; + + // 输入一个数组,构造前缀和 + public NumArray(int[] nums) { + // preSum[0] = 0,便于计算累加和 + preSum = new int[nums.length + 1]; + // 计算 nums 的累加和 + for (int i = 1; i < preSum.length; i++) { + preSum[i] = preSum[i - 1] + nums[i - 1]; + } + } + + // 查询闭区间 [left, right] 的累加和 + public int sumRange(int left, int right) { + return preSum[right + 1] - preSum[left]; } - return ans; } ``` -比如说下面这个情况,需要前缀和 8 就能找到和为 k 的子数组了,之前的暴力解法需要遍历数组去数有几个 8,而优化解法借助哈希表可以直接得知有几个前缀和为 8。 +核心思路是我们 new 一个新的数组 `preSum` 出来,`preSum[i]` 记录 `nums[0..i-1]` 的累加和,看图 $10 = 3 + 5 + 2$: + +![](https://labuladong.online/algo/images/difference/1.jpeg) + +看这个 `preSum` 数组,如果我想求索引区间 `[1, 4]` 内的所有元素之和,就可以通过 `preSum[5] - preSum[1]` 得出。 + +这样,`sumRange` 函数仅仅需要做一次减法运算,避免了每次进行 for 循环调用,最坏时间复杂度为常数 $O(1)$。 -![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/2.jpg) + + +你可以点开下面的可视化动画,点击 preSum[i] = preSum[i - 1] + nums[i - 1] 这行代码,即可看到 `preSum` 数组的计算,多次点击 console.log 这行代码,即可看到 `sumRange` 函数的调用: + + + +这个技巧在生活中运用也挺广泛的,比方说,你们班上有若干同学,每个同学有一个期末考试的成绩(满分 100 分),那么请你实现一个 API,输入任意一个分数段,返回有多少同学的成绩在这个分数段内。 + +那么,你可以先通过计数排序的方式计算每个分数具体有多少个同学,然后利用前缀和技巧来实现分数段查询的 API: -这样,就把时间复杂度降到了 $O(N)$,是最优解法了。 -### 三、总结 -前缀和不难,却很有用,主要用于处理数组区间的问题。 -比如说,让你统计班上同学考试成绩在不同分数段的百分比,也可以利用前缀和技巧: ```java -int[] scores; // 存储着所有同学的分数 -// 试卷满分 150 分 -int[] count = new int[150 + 1] +// 存储着所有同学的分数 +int[] scores; +// 试卷满分 100 分 +int[] count = new int[100 + 1]; // 记录每个分数有几个同学 for (int score : scores) - count[score]++ + count[score]++; // 构造前缀和 for (int i = 1; i < count.length; i++) count[i] = count[i] + count[i-1]; +// 利用 count 这个前缀和数组进行分数段查询 ``` -这样,给你任何一个分数段,你都能通过前缀和相减快速计算出这个分数段的人数,百分比也就很容易计算了。 -但是,稍微复杂一些的算法问题,不止考察简单的前缀和技巧。比如本文探讨的这道题目,就需要借助前缀和的思路做进一步的优化,借助哈希表去除不必要的嵌套循环。可见对题目的理解和细节的分析能力对于算法的优化是至关重要的。 -希望本文对你有帮助。 +接下来,我们看一看前缀和思路在二维数组中如何运用。 + +## 二维矩阵中的前缀和 + +这是力扣第 304 题「二维区域和检索 - 矩阵不可变」,其实和上一题类似,上一题是让你计算子数组的元素之和,这道题让你计算二维矩阵中子矩阵的元素之和: + + + +当然,你可以用一个嵌套 for 循环去遍历这个矩阵,但这样的话 `sumRegion` 函数的时间复杂度就高了,你算法的格局就低了。 + +注意任意子矩阵的元素和可以转化成它周边几个大矩阵的元素和的运算: + +![](https://labuladong.online/algo/images/presum/5.jpeg) + +而这四个大矩阵有一个共同的特点,就是左上角都是 `(0, 0)` 原点。 + +那么做这道题更好的思路和一维数组中的前缀和是非常类似的,我们可以维护一个二维 `preSum` 数组,专门记录以原点为顶点的矩阵的元素之和,就可以用几次加减运算算出任何一个子矩阵的元素和: + +```java +class NumMatrix { + // preSum[i][j] 记录矩阵 [0, 0, i-1, j-1] 的元素和 + private int[][] preSum; + + public NumMatrix(int[][] matrix) { + int m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return; + // 构造前缀和矩阵 + preSum = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 计算每个矩阵 [0, 0, i, j] 的元素和 + preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1]; + } + } + } + + // 计算子矩阵 [x1, y1, x2, y2] 的元素和 + public int sumRegion(int x1, int y1, int x2, int y2) { + // 目标矩阵之和由四个相邻矩阵运算获得 + return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1]; + } +} +``` + +这样,`sumRegion` 函数的时间复杂度也用前缀和技巧优化到了 $O(1)$,这是典型的「空间换时间」思路。 + + + +你可以点开下面的可视化动画,多次点击 preSum[i][j] = ... 这一行代码,即可看到 preSum 数组的计算过程,多次点击 console.log 这一行代码,即可看到 sumRegion 函数的调用: + + + +前缀和技巧就讲到这里,应该说这个算法技巧是会者不难难者不会,实际运用中还是要多培养自己的思维灵活性,做到一眼看出题目是一个前缀和问题。 + + + + + + + +## 拓展延伸 + +本文讲解的前缀和技巧是利用预计算的 `preSum` 数组快速计算索引区间内的元素和,但实际上它不仅仅局限于求和,也可以用来快速计算区间内的最大值、最小值、乘积等等。 + +而且前缀和数组经常和其他数据结构或算法技巧相结合,我会在 [前缀和技巧高频习题](https://labuladong.online/algo/problem-set/perfix-sum/) 中结合习题讲解。 + +**还有一个重要的问题:使用前缀和技巧的前提是原数组 `nums` 不会发生变化**。 + +如果原数组中的某个元素改变了,那么 `preSum` 数组中该元素后面的值就会失效,需要重新花费 $O(n)$ 的时间计算,这就和普通的暴力解法没太大区别了。 + +所以在数组元素可变的场景下,我们不能使用前缀和技巧,而是使用 [线段树](https://labuladong.online/algo/data-structure/segment-tree-implement/) 这种数据结构来处理区间查询和动态更新。 + + + + + + + + +
+
+引用本文的文章 + + - [SegmentTree 线段树代码实现](https://labuladong.online/algo/data-structure/segment-tree-implement/) + - [【强化练习】前缀和技巧经典习题](https://labuladong.online/algo/problem-set/perfix-sum/) + - [【强化练习】单调队列的通用实现及经典习题](https://labuladong.online/algo/problem-set/monotonic-queue/) + - [【强化练习】用「遍历」思维解题 III](https://labuladong.online/algo/problem-set/binary-tree-traverse-iii/) + - [二维数组的花式遍历技巧](https://labuladong.online/algo/practice-in-action/2d-array-traversal-summary/) + - [动态规划设计:最大子数组](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [小而美的算法技巧:差分数组](https://labuladong.online/algo/data-structure/diff-array/) + - [带权重的随机选择算法](https://labuladong.online/algo/frequency-interview/random-pick-with-weight/) + - [拓展:归并排序详解及应用](https://labuladong.online/algo/practice-in-action/merge-sort/) + - [算法刷题的重点和坑](https://labuladong.online/algo/intro/how-to-learn-algorithms/) + - [线段树核心原理及可视化](https://labuladong.online/algo/data-structure-basic/segment-tree-basic/) + - [选择排序所面临的问题](https://labuladong.online/algo/data-structure-basic/select-sort/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1314. Matrix Block Sum](https://leetcode.com/problems/matrix-block-sum/?show=1) | [1314. 矩阵区域和](https://leetcode.cn/problems/matrix-block-sum/?show=1) | 🟠 | +| [1352. Product of the Last K Numbers](https://leetcode.com/problems/product-of-the-last-k-numbers/?show=1) | [1352. 最后 K 个数的乘积](https://leetcode.cn/problems/product-of-the-last-k-numbers/?show=1) | 🟠 | +| [238. Product of Array Except Self](https://leetcode.com/problems/product-of-array-except-self/?show=1) | [238. 除自身以外数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/?show=1) | 🟠 | +| [325. Maximum Size Subarray Sum Equals k](https://leetcode.com/problems/maximum-size-subarray-sum-equals-k/?show=1)🔒 | [325. 和等于 k 的最长子数组长度](https://leetcode.cn/problems/maximum-size-subarray-sum-equals-k/?show=1)🔒 | 🟠 | +| [327. Count of Range Sum](https://leetcode.com/problems/count-of-range-sum/?show=1) | [327. 区间和的个数](https://leetcode.cn/problems/count-of-range-sum/?show=1) | 🔴 | +| [437. Path Sum III](https://leetcode.com/problems/path-sum-iii/?show=1) | [437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/?show=1) | 🟠 | +| [523. Continuous Subarray Sum](https://leetcode.com/problems/continuous-subarray-sum/?show=1) | [523. 连续的子数组和](https://leetcode.cn/problems/continuous-subarray-sum/?show=1) | 🟠 | +| [525. Contiguous Array](https://leetcode.com/problems/contiguous-array/?show=1) | [525. 连续数组](https://leetcode.cn/problems/contiguous-array/?show=1) | 🟠 | +| [560. Subarray Sum Equals K](https://leetcode.com/problems/subarray-sum-equals-k/?show=1) | [560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/?show=1) | 🟠 | +| [724. Find Pivot Index](https://leetcode.com/problems/find-pivot-index/?show=1) | [724. 寻找数组的中心下标](https://leetcode.cn/problems/find-pivot-index/?show=1) | 🟢 | +| [862. Shortest Subarray with Sum at Least K](https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/?show=1) | [862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/?show=1) | 🔴 | +| [918. Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray/?show=1) | [918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/?show=1) | 🟠 | +| [974. Subarray Sums Divisible by K](https://leetcode.com/problems/subarray-sums-divisible-by-k/?show=1) | [974. 和可被 K 整除的子数组](https://leetcode.cn/problems/subarray-sums-divisible-by-k/?show=1) | 🟠 | +| - | [剑指 Offer 57 - II. 和为s的连续正数序列](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/?show=1) | 🟢 | +| - | [剑指 Offer II 010. 和为 k 的子数组](https://leetcode.cn/problems/QTMn0o/?show=1) | 🟠 | +| - | [剑指 Offer II 011. 0 和 1 个数相同的子数组](https://leetcode.cn/problems/A1NYOS/?show=1) | 🟠 | +| - | [剑指 Offer II 012. 左右两边子数组的和相等](https://leetcode.cn/problems/tvdfij/?show=1) | 🟢 | +| - | [剑指 Offer II 013. 二维子矩阵的和](https://leetcode.cn/problems/O4NDxx/?show=1) | 🟠 | +| - | [剑指 Offer II 050. 向下的路径节点之和](https://leetcode.cn/problems/6eUYwP/?show=1) | 🟠 | + +
+
-坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:烧饼排序](../算法思维系列/烧饼排序.md) -[下一篇:字符串乘法](../算法思维系列/字符串乘法.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" deleted file mode 100644 index 7a5552e97c..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" +++ /dev/null @@ -1,115 +0,0 @@ -# 区间交集问题 - -本文是区间系列问题的第三篇,前两篇分别讲了区间的最大不相交子集和重叠区间的合并,今天再写一个算法,可以快速找出两组区间的交集。 - -先看下题目,LeetCode 第 986 题就是这个问题: - -![title](../pictures/intersection/title.png) - -题目很好理解,就是让你找交集,注意区间都是闭区间。 - -### 思路 - -解决区间问题的思路一般是先排序,以便操作,不过题目说已经排好序了,那么可以用两个索引指针在 `A` 和 `B` 中游走,把交集找出来,代码大概是这样的: - -```python -# A, B 形如 [[0,2],[5,10]...] -def intervalIntersection(A, B): - i, j = 0, 0 - res = [] - while i < len(A) and j < len(B): - # ... - j += 1 - i += 1 - return res -``` - -不难,我们先老老实实分析一下各种情况。 - -首先,**对于两个区间**,我们用 `[a1,a2]` 和 `[b1,b2]` 表示在 `A` 和 `B` 中的两个区间,那么什么情况下这两个区间**没有交集**呢: - -![](../pictures/intersection/1.jpg) - -只有这两种情况,写成代码的条件判断就是这样: - -```python -if b2 < a1 or a2 < b1: - [a1,a2] 和 [b1,b2] 无交集 -``` - -那么,什么情况下,两个区间存在交集呢?根据命题的否定,上面逻辑的否命题就是存在交集的条件: - -```python -# 不等号取反,or 也要变成 and -if b2 >= a1 and a2 >= b1: - [a1,a2] 和 [b1,b2] 存在交集 -``` - -接下来,两个区间存在交集的情况有哪些呢?穷举出来: - -![](../pictures/intersection/2.jpg) - -这很简单吧,就这四种情况而已。那么接下来思考,这几种情况下,交集是否有什么共同点呢? - -![](../pictures/intersection/3.jpg) - -我们惊奇地发现,交集区间是有规律的!如果交集区间是 `[c1,c2]`,那么 `c1=max(a1,b1)`,`c2=min(a2,b2)`!这一点就是寻找交集的核心,我们把代码更进一步: - -```python -while i < len(A) and j < len(B): - a1, a2 = A[i][0], A[i][1] - b1, b2 = B[j][0], B[j][1] - if b2 >= a1 and a2 >= b1: - res.append([max(a1, b1), min(a2, b2)]) - # ... -``` - -最后一步,我们的指针 `i` 和 `j` 肯定要前进(递增)的,什么时候应该前进呢? - -![](../pictures/intersection/4.gif) - -结合动画示例就很好理解了,是否前进,只取决于 `a2` 和 `b2` 的大小关系: - -```python -while i < len(A) and j < len(B): - # ... - if b2 < a2: - j += 1 - else: - i += 1 -``` - -### 代码 - -```python -# A, B 形如 [[0,2],[5,10]...] -def intervalIntersection(A, B): - i, j = 0, 0 # 双指针 - res = [] - while i < len(A) and j < len(B): - a1, a2 = A[i][0], A[i][1] - b1, b2 = B[j][0], B[j][1] - # 两个区间存在交集 - if b2 >= a1 and a2 >= b1: - # 计算出交集,加入 res - res.append([max(a1, b1), min(a2, b2)]) - # 指针前进 - if b2 < a2: j += 1 - else: i += 1 - return res -``` - -总结一下,区间类问题看起来都比较复杂,情况很多难以处理,但实际上通过观察各种不同情况之间的共性可以发现规律,用简洁的代码就能处理。 - -另外,区间问题没啥特别厉害的奇技淫巧,其操作也朴实无华,但其应用却十分广泛,接之前的几篇文章: - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:区间调度之区间合并问题](../算法思维系列/区间调度问题之区间合并.md) - -[下一篇:信封嵌套问题](../算法思维系列/信封嵌套问题.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" deleted file mode 100644 index bc0fd37c12..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" +++ /dev/null @@ -1,73 +0,0 @@ -# 区间调度问题之区间合并 - -上篇文章用贪心算法解决了区间调度问题:给你很多区间,让你求其中的最大不重叠子集。 - -其实对于区间相关的问题,还有很多其他类型,本文就来讲讲区间合并问题(Merge Interval)。 - -LeetCode 第 56 题就是一道相关问题,题目很好理解: - -![title](../pictures/mergeInterval/title.png) - -我们解决区间问题的一般思路是先排序,然后观察规律。 - -### 一、思路 - -一个区间可以表示为 `[start, end]`,前文聊的区间调度问题,需要按 `end` 排序,以便满足贪心选择性质。而对于区间合并问题,其实按 `end` 和 `start` 排序都可以,不过为了清晰起见,我们选择按 `start` 排序。 - -![1](../pictures/mergeInterval/1.jpg) - -**显然,对于几个相交区间合并后的结果区间 `x`,`x.start` 一定是这些相交区间中 `start` 最小的,`x.end` 一定是这些相交区间中 `end` 最大的。** - -![2](../pictures/mergeInterval/2.jpg) - -由于已经排了序,`x.start` 很好确定,求 `x.end` 也很容易,可以类比在数组中找最大值的过程: - -```java -int max_ele = arr[0]; -for (int i = 1; i < arr.length; i++) - max_ele = max(max_ele, arr[i]); -return max_ele; -``` - -### 二、代码 - -```python -# intervals 形如 [[1,3],[2,6]...] -def merge(intervals): - if not intervals: return [] - # 按区间的 start 升序排列 - intervals.sort(key=lambda intv: intv[0]) - res = [] - res.append(intervals[0]) - - for i in range(1, len(intervals)): - curr = intervals[i] - # res 中最后一个元素的引用 - last = res[-1] - if curr[0] <= last[1]: - # 找到最大的 end - last[1] = max(last[1], curr[1]) - else: - # 处理下一个待合并区间 - res.append(curr) - return res -``` - -看下动画就一目了然了: - -![3](../pictures/mergeInterval/3.gif) - -至此,区间合并问题就解决了。本文篇幅短小,因为区间合并只是区间问题的一个类型,后续还有一些区间问题。本想把所有问题类型都总结在一篇文章,但有读者反应,长文只会收藏不会看... 所以还是分成小短文吧,读者有什么看法可以在留言板留言交流。 - -本文终,希望对你有帮助。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:FloodFill算法详解及应用](../算法思维系列/FloodFill算法详解及应用.md) - -[下一篇:区间调度之区间交集问题](../算法思维系列/区间交集问题.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" index 51d821f66d..6662d7a004 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" @@ -1,138 +1,278 @@ -# 双指针技巧总结 +# 双指针技巧秒杀七道数组题目 -我把双指针技巧再分为两类,一类是「快慢指针」,一类是「左右指针」。前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环;后者主要解决数组(或者字符串)中的问题,比如二分查找。 -### 一、快慢指针的常见算法 -快慢指针一般都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后,巧妙解决一些链表中的问题。 +![](https://labuladong.online/algo/images/souyisou1.png) -**1、判定链表中是否含有环** +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -这应该属于链表最基本的操作了,如果读者已经知道这个技巧,可以跳过。 -单链表的特点是每个节点只知道下一个节点,所以一个指针的话无法判断链表中是否含有环的。 -如果链表中不含环,那么这个指针最终会遇到空指针 null 表示链表到头了,这还好说,可以判断该链表不含环。 +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [167. Two Sum II - Input Array Is Sorted](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/) | [167. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) | 🟠 | +| [26. Remove Duplicates from Sorted Array](https://leetcode.com/problems/remove-duplicates-from-sorted-array/) | [26. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) | 🟢 | +| [27. Remove Element](https://leetcode.com/problems/remove-element/) | [27. 移除元素](https://leetcode.cn/problems/remove-element/) | 🟢 | +| [283. Move Zeroes](https://leetcode.com/problems/move-zeroes/) | [283. 移动零](https://leetcode.cn/problems/move-zeroes/) | 🟢 | +| [344. Reverse String](https://leetcode.com/problems/reverse-string/) | [344. 反转字符串](https://leetcode.cn/problems/reverse-string/) | 🟢 | +| [5. Longest Palindromic Substring](https://leetcode.com/problems/longest-palindromic-substring/) | [5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | 🟠 | +| [83. Remove Duplicates from Sorted List](https://leetcode.com/problems/remove-duplicates-from-sorted-list/) | [83. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) | 🟢 | +| - | [剑指 Offer 57. 和为s的两个数字](https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/) | 🟢 | +| - | [剑指 Offer II 006. 排序数组中两个数字之和](https://leetcode.cn/problems/kLl5u1/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) +> - [单链表的六大解题套路](https://labuladong.online/algo/essential-technique/linked-list-skills-summary/) + + +在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:**左右指针**和**快慢指针**。 + +所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。 + +对于单链表来说,大部分技巧都属于快慢指针,[单链表的六大解题套路](https://labuladong.online/algo/essential-technique/linked-list-skills-summary/) 都涵盖了,比如链表环判断,倒数第 `K` 个链表节点等问题,它们都是通过一个 `fast` 快指针和一个 `slow` 慢指针配合完成任务。 + +在数组中并没有真正意义上的指针,但我们可以把索引当做数组中的指针,这样也可以在数组中施展双指针技巧,**本文主要讲数组相关的双指针算法**。 + +## 一、快慢指针技巧 + +### 原地修改 + +**数组问题中比较常见的快慢指针技巧,是让你原地修改数组**。 + +比如说看下力扣第 26 题「删除有序数组中的重复项」,让你在有序数组去重: + + + +函数签名如下: + ```java +int removeDuplicates(int[] nums); +``` + +简单解释一下什么是原地修改: + +如果不是原地修改的话,我们直接 new 一个 `int[]` 数组,把去重之后的元素放进这个新数组中,然后返回这个新数组即可。 + +但是现在题目让你原地删除,不允许 new 新数组,只能在原数组上操作,然后返回一个长度,这样就可以通过返回的长度和原始数组得到我们去重后的元素有哪些了。 + +由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难。但如果毎找到一个重复元素就立即原地删除它,由于数组中删除元素涉及数据搬移,整个时间复杂度是会达到 $O(N^2)$。 + +高效解决这道题就要用到快慢指针技巧: + +我们让慢指针 `slow` 走在后面,快指针 `fast` 走在前面探路,找到一个不重复的元素就赋值给 `slow` 并让 `slow` 前进一步。 -boolean hasCycle(ListNode head) { - while (head != null) - head = head.next; - return false; +这样,就保证了 `nums[0..slow]` 都是无重复的元素,当 `fast` 指针遍历完整个数组 `nums` 后,`nums[0..slow]` 就是整个数组去重之后的结果。 + +看代码: + +```java +class Solution { + public int removeDuplicates(int[] nums) { + if (nums.length == 0) { + return 0; + } + int slow = 0, fast = 0; + while (fast < nums.length) { + if (nums[fast] != nums[slow]) { + slow++; + // 维护 nums[0..slow] 无重复 + nums[slow] = nums[fast]; + } + fast++; + } + // 数组长度为索引 + 1 + return slow + 1; + } } ``` -但是如果链表中含有环,那么这个指针就会陷入死循环,因为环形数组中没有 null 指针作为尾部节点。 + + +你可以打开下面的可视化面板,多次点击 while (fast < nums.length) 这一行代码,即可看到两个指针维护 `nums[0..slow]` 无重复元素: -经典解法就是用两个指针,一个跑得快,一个跑得慢。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。 + + +再简单扩展一下,看看力扣第 83 题「删除排序链表中的重复元素」,如果给你一个有序的单链表,如何去重呢? + +其实和数组去重是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已,你对照着之前的代码来看: ```java -boolean hasCycle(ListNode head) { - ListNode fast, slow; - fast = slow = head; - while (fast != null && fast.next != null) { - fast = fast.next.next; - slow = slow.next; - - if (fast == slow) return true; +class Solution { + public ListNode deleteDuplicates(ListNode head) { + if (head == null) return null; + ListNode slow = head, fast = head; + while (fast != null) { + if (fast.val != slow.val) { + // nums[slow] = nums[fast]; + slow.next = fast; + // slow++; + slow = slow.next; + } + // fast++ + fast = fast.next; + } + // 断开与后面重复元素的连接 + slow.next = null; + return head; } - return false; } ``` -**2、已知链表中含有环,返回这个环的起始位置** +算法执行的过程请看下面这个可视化面板: + + +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
+ +> [!NOTE] +> 这里可能有读者会问,链表中那些重复的元素并没有被删掉,就让这些节点在链表上挂着,合适吗? +> +> 这就要探讨不同语言的特性了,像 Java/Python 这类带有垃圾回收的语言,可以帮我们自动找到并回收这些「悬空」的链表节点的内存,而像 C++ 这类语言没有自动垃圾回收的机制,确实需要我们编写代码时手动释放掉这些节点的内存。 +> +> 不过话说回来,就算法思维的培养来说,我们只需要知道这种快慢指针技巧即可。 + +**除了让你在有序数组/链表中去重,题目还可能让你对数组中的某些元素进行「原地删除」**。 + +比如力扣第 27 题「移除元素」,看下题目: -![1](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/1.png) -这个问题一点都不困难,有点类似脑筋急转弯,先直接看代码: + + + + ```java -ListNode detectCycle(ListNode head) { - ListNode fast, slow; - fast = slow = head; - while (fast != null && fast.next != null) { - fast = fast.next.next; - slow = slow.next; - if (fast == slow) break; - } - // 上面的代码类似 hasCycle 函数 - slow = head; - while (slow != fast) { - fast = fast.next; - slow = slow.next; +// 函数签名如下 +int removeElement(int[] nums, int val); +``` + +题目要求我们把 `nums` 中所有值为 `val` 的元素原地删除,依然需要使用快慢指针技巧: + +如果 `fast` 遇到值为 `val` 的元素,则直接跳过,否则就赋值给 `slow` 指针,并让 `slow` 前进一步。 + +这和前面说到的数组去重问题解法思路是完全一样的,直接看代码: + +```java +class Solution { + public int removeElement(int[] nums, int val) { + int fast = 0, slow = 0; + while (fast < nums.length) { + if (nums[fast] != val) { + nums[slow] = nums[fast]; + slow++; + } + fast++; + } + return slow; } - return slow; } ``` -可以看到,当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢? + -第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。 +你可以打开下面的可视化面板,多次点击 while (fast !== null) 这一行代码,即可看到两个指针维护 `nums[0..slow]` 无重复元素: -![2](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/2.png) + -设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。 +注意这里和有序数组去重的解法有一个细节差异,我们这里是先给 `nums[slow]` 赋值然后再给 `slow++`,这样可以保证 `nums[0..slow-1]` 是不包含值为 `val` 的元素的,最后的结果数组长度就是 `slow`。 -巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。 +实现了这个 `removeElement` 函数,接下来看看力扣第 283 题「移动零」: + +给你输入一个数组 `nums`,请你**原地修改**,将数组中的所有值为 0 的元素移到数组末尾,函数签名如下: + +```java +void moveZeroes(int[] nums); +``` -![3](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/3.png) +比如说给你输入 `nums = [0,1,4,0,2]`,你的算法没有返回值,但是会把 `nums` 数组原地修改成 `[1,4,2,0,0]`。 -所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。 +结合之前说到的几个题目,你是否有已经有了答案呢? -**3、寻找链表的中点** +稍微修改上一题中的 `removeElement` 函数就可以完成这道题,或者直接复用 `removeElement` 函数也可以。 -类似上面的思路,我们还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。 +题目让我们将所有 0 移到最后,其实就相当于移除 `nums` 中的所有 0,然后再把后面的元素都赋值为 0: ```java -while (fast != null && fast.next != null) { - fast = fast.next.next; - slow = slow.next; +class Solution { + public void moveZeroes(int[] nums) { + // 去除 nums 中的所有 0,返回不含 0 的数组长度 + int p = removeElement(nums, 0); + // 将 nums[p..] 的元素赋值为 0 + for (; p < nums.length; p++) { + nums[p] = 0; + } + } + + public int removeElement(int[] nums, int val) { + // 见上文代码实现 + } } -// slow 就在中间位置 -return slow; ``` -当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右: + -![center](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/center.png) +你可以点开下面的可视化面板,多次点击 while (fast < nums.length) 这行代码查看快慢指针的运动,然后多次点击 nums[p] = 0; 这行代码将后面的元素都改为 0: -寻找链表中点的一个重要作用是对链表进行归并排序。 + -回想数组的归并排序:求中点索引递归地把数组二分,最后合并两个有序数组。对于链表,合并两个有序链表是很简单的,难点就在于二分。 +到这里,原地修改数组的这些题目就已经差不多了。 + +### 滑动窗口 + +数组中另一大类快慢指针的题目就是「滑动窗口算法」。我在另一篇文章 [滑动窗口算法核心框架详解](https://labuladong.online/algo/essential-technique/sliding-window-framework/) 给出了滑动窗口的代码框架: -但是现在你学会了找到链表的中点,就能实现链表的二分了。关于归并排序的具体内容本文就不具体展开了。 -**4、寻找链表的倒数第 k 个元素** -我们的思路还是使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点(为了简化,假设 k 不会超过链表长度): ```java -ListNode slow, fast; -slow = fast = head; -while (k-- > 0) - fast = fast.next; - -while (fast != null) { - slow = slow.next; - fast = fast.next; +// 滑动窗口算法框架伪码 +int left = 0, right = 0; + +while (right < nums.size()) { + // 增大窗口 + window.addLast(nums[right]); + right++; + + while (window needs shrink) { + // 缩小窗口 + window.removeFirst(nums[left]); + left++; + } } -return slow; ``` -### 二、左右指针的常用算法 -左右指针在数组中实际是指两个索引值,一般初始化为 left = 0, right = nums.length - 1 。 +具体的题目本文就不重复了,这里只强调滑动窗口算法的快慢指针特性: -**1、二分查找** +`left` 指针在后,`right` 指针在前,两个指针中间的部分就是「窗口」,算法通过扩大和缩小「窗口」来解决某些问题。 -前文「二分查找」有详细讲解,这里只写最简单的二分算法,旨在突出它的双指针特性: +## 二、左右指针的常用算法 + +### 二分查找 + +我在另一篇文章 [二分查找框架详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) 中有详细探讨二分搜索代码的细节问题,这里只写最简单的二分算法,旨在突出它的双指针特性: ```java int binarySearch(int[] nums, int target) { - int left = 0; - int right = nums.length - 1; + // 一左一右两个指针相向而行 + int left = 0, right = nums.length - 1; while(left <= right) { int mid = (right + left) / 2; if(nums[mid] == target) @@ -146,60 +286,221 @@ int binarySearch(int[] nums, int target) { } ``` -**2、两数之和** +### `n` 数之和 -直接看一道 LeetCode 题目吧: +看下力扣第 167 题「两数之和 II」: -![title](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/title.png) + -只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 可以调整 sum 的大小: +只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 `left` 和 `right` 就可以调整 `sum` 的大小: ```java -int[] twoSum(int[] nums, int target) { - int left = 0, right = nums.length - 1; - while (left < right) { - int sum = nums[left] + nums[right]; - if (sum == target) { - // 题目要求的索引是从 1 开始的 - return new int[]{left + 1, right + 1}; - } else if (sum < target) { - left++; // 让 sum 大一点 - } else if (sum > target) { - right--; // 让 sum 小一点 +class Solution { + public int[] twoSum(int[] numbers, int target) { + // 一左一右两个指针相向而行 + int left = 0, right = numbers.length - 1; + while (left < right) { + int sum = numbers[left] + numbers[right]; + if (sum == target) { + // 题目要求的索引是从 1 开始的 + return new int[]{left + 1, right + 1}; + } else if (sum < target) { + // 让 sum 大一点 + left++; + } else if (sum > target) { + // 让 sum 小一点 + right--; + } } + return new int[]{-1, -1}; } - return new int[]{-1, -1}; } ``` -**3、反转数组** +我在另一篇文章 [一个函数秒杀所有 nSum 问题](https://labuladong.online/algo/practice-in-action/nsum/) 中也运用类似的左右指针技巧给出了 `nSum` 问题的一种通用思路,本质上利用的也是双指针技巧。 + +### 反转数组 + +一般编程语言都会提供 `reverse` 函数,其实这个函数的原理非常简单,力扣第 344 题「反转字符串」就是类似的需求,让你反转一个 `char[]` 类型的字符数组,我们直接看代码吧: ```java -void reverse(int[] nums) { - int left = 0; - int right = nums.length - 1; +void reverseString(char[] s) { + // 一左一右两个指针相向而行 + int left = 0, right = s.length - 1; while (left < right) { - // swap(nums[left], nums[right]) - int temp = nums[left]; - nums[left] = nums[right]; - nums[right] = temp; - left++; right--; + // 交换 s[left] 和 s[right] + char temp = s[left]; + s[left] = s[right]; + s[right] = temp; + left++; + right--; } } ``` -**4、滑动窗口算法** +关于数组翻转的更多进阶问题,可以参见 [二维数组的花式遍历](https://labuladong.online/algo/practice-in-action/2d-array-traversal-summary/)。 + +### 回文串判断 + +回文串就是正着读和反着读都一样的字符串。比如说字符串 `aba` 和 `abba` 都是回文串,因为它们对称,反过来还是和本身一样;反之,字符串 `abac` 就不是回文串。 + +现在你应该能感觉到回文串问题和左右指针肯定有密切的联系,比如让你判断一个字符串是不是回文串,你可以写出下面这段代码: + +```java +boolean isPalindrome(String s) { + // 一左一右两个指针相向而行 + int left = 0, right = s.length() - 1; + while (left < right) { + if (s.charAt(left) != s.charAt(right)) { + return false; + } + left++; + right--; + } + return true; +} +``` + +那接下来我提升一点难度,给你一个字符串,让你用双指针技巧从中找出最长的回文串,你会做吗? + +这就是力扣第 5 题「最长回文子串」: + + + +函数签名如下: + +```java +String longestPalindrome(String s); +``` + +找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是**从中心向两端扩散的双指针技巧**。 + +如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。所以我们可以先实现这样一个函数: + +```java +// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串 +String palindrome(String s, int l, int r) { + // 防止索引越界 + while (l >= 0 && r < s.length() + && s.charAt(l) == s.charAt(r)) { + // 双指针,向两边展开 + l--; r++; + } + // 返回以 s[l] 和 s[r] 为中心的最长回文串 + return s.substring(l + 1, r); +} +``` + +这样,如果输入相同的 `l` 和 `r`,就相当于寻找长度为奇数的回文串,如果输入相邻的 `l` 和 `r`,则相当于寻找长度为偶数的回文串。 + +那么回到最长回文串的问题,解法的大致思路就是: + +```python +for 0 <= i < len(s): + 找到以 s[i] 为中心的回文串 + 找到以 s[i] 和 s[i+1] 为中心的回文串 + 更新答案 +``` + +翻译成代码,就可以解决最长回文子串这个问题: + +```java +String longestPalindrome(String s) { + String res = ""; + for (int i = 0; i < s.length(); i++) { + // 以 s[i] 为中心的最长回文子串 + String s1 = palindrome(s, i, i); + // 以 s[i] 和 s[i+1] 为中心的最长回文子串 + String s2 = palindrome(s, i, i + 1); + // res = longest(res, s1, s2) + res = res.length() > s1.length() ? res : s1; + res = res.length() > s2.length() ? res : s2; + } + return res; +} +``` + + + +你可以点开下面的可视化面板,多次点击 while (l >= 0 && r < s.length && s[l] === s[r]) 这一行代码,即可看到 `l, r` 两个指针向两边展开的过程: + + + +你应该能发现最长回文子串使用的左右指针和之前题目的左右指针有一些不同:之前的左右指针都是从两端向中间相向而行,而回文子串问题则是让左右指针从中心向两端扩展。不过这种情况也就回文串这类问题会遇到,所以我也把它归为左右指针了。 + +到这里,数组相关的双指针技巧就全部讲完了,这些技巧的更多扩展延伸见 [更多数组双指针经典高频题](https://labuladong.online/algo/problem-set/array-two-pointers/)。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】二分搜索算法经典习题](https://labuladong.online/algo/problem-set/binary-search/) + - [【强化练习】哈希表更多习题](https://labuladong.online/algo/problem-set/hash-table/) + - [【强化练习】回溯算法经典习题 I](https://labuladong.online/algo/problem-set/backtrack-i/) + - [【强化练习】回溯算法经典习题 III](https://labuladong.online/algo/problem-set/backtrack-iii/) + - [【强化练习】数学技巧相关习题](https://labuladong.online/algo/problem-set/math-tricks/) + - [【强化练习】数组双指针经典习题](https://labuladong.online/algo/problem-set/array-two-pointers/) + - [【强化练习】更多经典设计习题](https://labuladong.online/algo/problem-set/ds-design/) + - [【强化练习】链表双指针经典习题](https://labuladong.online/algo/problem-set/linkedlist-two-pointers/) + - [一个方法团灭 nSum 问题](https://labuladong.online/algo/practice-in-action/nsum/) + - [二维数组的花式遍历技巧](https://labuladong.online/algo/practice-in-action/2d-array-traversal-summary/) + - [动态规划之子序列问题解题模板](https://labuladong.online/algo/dynamic-programming/subsequence-problem/) + - [博采众长:桶排序](https://labuladong.online/algo/data-structure-basic/bucket-sort/) + - [如何判断回文链表](https://labuladong.online/algo/data-structure/palindrome-linked-list/) + - [如何高效解决接雨水问题](https://labuladong.online/algo/frequency-interview/trapping-rain-water/) + - [妙用二叉树前序位置:快速排序](https://labuladong.online/algo/data-structure-basic/quick-sort/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [扫描线技巧:安排会议室](https://labuladong.online/algo/frequency-interview/scan-line-technique/) + - [拓展:数组去重问题(困难版)](https://labuladong.online/algo/frequency-interview/remove-duplicate-letters/) + - [滑动窗口算法核心代码模板](https://labuladong.online/algo/essential-technique/sliding-window-framework/) + - [用算法打败算法](https://labuladong.online/algo/fname.html?fname=PDF中的算法) + - [田忌赛马背后的算法决策](https://labuladong.online/algo/practice-in-action/advantage-shuffle/) + - [算法刷题的重点和坑](https://labuladong.online/algo/intro/how-to-learn-algorithms/) + - [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + - [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1. Two Sum](https://leetcode.com/problems/two-sum/?show=1) | [1. 两数之和](https://leetcode.cn/problems/two-sum/?show=1) | 🟢 | +| [125. Valid Palindrome](https://leetcode.com/problems/valid-palindrome/?show=1) | [125. 验证回文串](https://leetcode.cn/problems/valid-palindrome/?show=1) | 🟢 | +| [131. Palindrome Partitioning](https://leetcode.com/problems/palindrome-partitioning/?show=1) | [131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/?show=1) | 🟠 | +| [267. Palindrome Permutation II](https://leetcode.com/problems/palindrome-permutation-ii/?show=1)🔒 | [267. 回文排列 II](https://leetcode.cn/problems/palindrome-permutation-ii/?show=1)🔒 | 🟠 | +| [281. Zigzag Iterator](https://leetcode.com/problems/zigzag-iterator/?show=1)🔒 | [281. 锯齿迭代器](https://leetcode.cn/problems/zigzag-iterator/?show=1)🔒 | 🟠 | +| [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/?show=1) | [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/?show=1) | 🔴 | +| [543. Diameter of Binary Tree](https://leetcode.com/problems/diameter-of-binary-tree/?show=1) | [543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/?show=1) | 🟢 | +| [658. Find K Closest Elements](https://leetcode.com/problems/find-k-closest-elements/?show=1) | [658. 找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/?show=1) | 🟠 | +| [75. Sort Colors](https://leetcode.com/problems/sort-colors/?show=1) | [75. 颜色分类](https://leetcode.cn/problems/sort-colors/?show=1) | 🟠 | +| [80. Remove Duplicates from Sorted Array II](https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/?show=1) | [80. 删除有序数组中的重复项 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/?show=1) | 🟠 | +| [82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/?show=1) | [82. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/?show=1) | 🟠 | +| [9. Palindrome Number](https://leetcode.com/problems/palindrome-number/?show=1) | [9. 回文数](https://leetcode.cn/problems/palindrome-number/?show=1) | 🟢 | +| - | [剑指 Offer 21. 调整数组顺序使奇数位于偶数前面](https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/?show=1) | 🟢 | +| - | [剑指 Offer 57. 和为s的两个数字](https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/?show=1) | 🟢 | +| - | [剑指 Offer II 018. 有效的回文](https://leetcode.cn/problems/XltzEq/?show=1) | 🟢 | -这也许是双指针技巧的最高境界了,如果掌握了此算法,可以解决一大类子字符串匹配的问题,不过「滑动窗口」稍微比上述的这些算法复杂些。 +
+
-幸运的是,这类算法是有框架模板的,而且[这篇文章](滑动窗口技巧.md)就讲解了「滑动窗口」算法模板,帮大家秒杀几道 LeetCode 子串匹配的问题。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:滑动窗口解题框架](../算法思维系列/滑动窗口技巧.md) -[下一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" index 192d4cfdfd..6a0a536cbc 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" @@ -1,8 +1,45 @@ -# 回溯算法详解 +# 回溯算法解题套路框架 -这篇文章是很久之前的一篇《回溯算法详解》的进阶版,之前那篇不够清楚,就不必看了,看这篇就行。把框架给你讲清楚,你会发现回溯算法问题都是一个套路。 -废话不多说,直接上回溯算法框架。**解决一个回溯问题,实际上就是一个决策树的遍历过程**。你只需要思考 3 个问题: + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [46. Permutations](https://leetcode.com/problems/permutations/) | [46. 全排列](https://leetcode.cn/problems/permutations/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树结构基础](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) +> - [二叉树的遍历框架](https://labuladong.online/algo/data-structure-basic/binary-tree-traverse-basic/) +> - [多叉树结构及遍历框架](https://labuladong.online/algo/data-structure-basic/n-ary-tree-traverse-basic/) + +> tip:本文有视频版:[回溯算法框架套路详解](https://www.bilibili.com/video/BV1P5411N7Xc/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +这篇文章是很久之前的一篇 [回溯算法详解](https://mp.weixin.qq.com/s/trILKSiN9EoS58pXmvUtUQ) 的进阶版。把框架给你讲清楚,你会发现回溯算法问题都是一个套路。 + +本文解决几个问题: + +回溯算法是什么?解决回溯算法相关的问题有什么技巧?如何学习回溯算法?回溯算法代码是否有规律可循? + +其实回溯算法和我们常说的 DFS 算法基本可以认为是同一种算法,它们的细微差异我在 [关于 DFS 和回溯算法的若干问题](https://labuladong.online/algo/essential-technique/backtrack-vs-dfs/) 中有详细解释,本文聚焦回溯算法,不展开。 + +**抽象地说,解决一个回溯问题,实际上就是遍历一棵决策树的过程,树的每个叶子节点存放着一个合法答案。你把整棵树遍历一遍,把叶子节点上的答案都收集起来,就能得到所有的合法答案**。 + +站在回溯树的一个节点上,你只需要思考 3 个问题: 1、路径:也就是已经做出的选择。 @@ -10,7 +47,7 @@ 3、结束条件:也就是到达决策树底层,无法再做选择的条件。 -如果你不理解这三个词语的解释,没关系,我们后面会用「全排列」和「N 皇后问题」这两个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象。 +如果你不理解这三个词语的解释,没关系,我们后面会用「全排列」这个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象。 代码方面,回溯算法的框架: @@ -27,60 +64,77 @@ def backtrack(路径, 选择列表): 撤销选择 ``` + + + + + + **其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」**,特别简单。 什么叫做选择和撤销选择呢,这个框架的底层原理是什么呢?下面我们就通过「全排列」这个问题来解开之前的疑惑,详细探究一下其中的奥妙! -### 一、全排列问题 +## 全排列问题解析 -我们在高中的时候就做过排列组合的数学题,我们也知道 `n` 个不重复的数,全排列共有 n! 个。 +力扣第 46 题「全排列」就是给你输入一个数组 `nums`,让你返回这些数字的全排列。 -PS:**为了简单清晰起见,我们这次讨论的全排列问题不包含重复的数字**。 +> [!NOTE] +> **我们这次讨论的全排列问题不包含重复的数字,包含重复数字的扩展场景我在后文 [回溯算法秒杀排列组合子集的九种题型](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) 中讲解**。 +> +> 另外,有些读者之前看过的全排列算法代码可能是那种 `swap` 交换元素的写法,和我在本文介绍的代码不同。这是回溯算法两种穷举思路,我会在后文 [球盒模型:回溯算法穷举的两种视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) 讲明白。现在还不适合直接跟你讲那个解法,你照着我的思路学习即可。 -那么我们当时是怎么穷举全排列的呢?比方说给三个数 `[1,2,3]`,你肯定不会无规律地乱穷举,一般是这样: +我们在高中的时候就做过排列组合的数学题,我们也知道 `n` 个不重复的数,全排列共有 `n!` 个。那么我们当时是怎么穷举全排列的呢? + +比方说给三个数 `[1,2,3]`,你肯定不会无规律地乱穷举,一般是这样: 先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位…… 其实这就是回溯算法,我们高中无师自通就会用,或者有的同学直接画出如下这棵回溯树: -![](../pictures/backtracking/1.jpg) +![](https://labuladong.online/algo/images/backtracking/1.jpg) 只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。**我们不妨把这棵树称为回溯算法的「决策树」**。 **为啥说这是决策树呢,因为你在每个节点上其实都在做决策**。比如说你站在下图的红色节点上: -![](../pictures/backtracking/2.jpg) +![](https://labuladong.online/algo/images/backtracking/2.jpg) 你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。 -**现在可以解答开头的几个名词:`[2]` 就是「路径」,记录你已经做过的选择;`[1,3]` 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层,在这里就是选择列表为空的时候**。 +**现在可以解答开头的几个名词:`[2]` 就是「路径」,记录你已经做过的选择;`[1,3]` 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层叶子节点,这里也就是选择列表为空的时候**。 -如果明白了这几个名词,**可以把「路径」和「选择」列表作为决策树上每个节点的属性**,比如下图列出了几个节点的属性: +如果明白了这几个名词,可以把「路径」和「选择」列表作为决策树上每个节点的属性,比如下图列出了几个蓝色节点的属性: -![](../pictures/backtracking/3.jpg) +![](https://labuladong.online/algo/images/backtracking/3.jpg) -**我们定义的 `backtrack` 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列**。 +**我们定义的 `backtrack` 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层叶子节点,其「路径」就是一个全排列**。 -再进一步,如何遍历一棵树?这个应该不难吧。回忆一下之前「学习数据结构的框架思维」写过,各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样: +再进一步,如何遍历一棵树?这个应该不难吧。回忆一下之前 [学习数据结构的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) 写过,各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样: ```java void traverse(TreeNode root) { - for (TreeNode child : root.childern) - // 前序遍历需要的操作 + for (TreeNode child : root.childern) { + // 前序位置需要的操作 traverse(child); - // 后序遍历需要的操作 + // 后序位置需要的操作 + } } ``` +> [!NOTE] +> 细心的读者肯定会疑问:多叉树 DFS 遍历框架的前序位置和后序位置应该在 for 循环外面,并不应该是在 for 循环里面呀?为什么在回溯算法中跑到 for 循环里面了? +> +> 是的,DFS 算法的前序和后序位置应该在 for 循环外面,不过回溯算法和 DFS 算法略有不同,[解答回溯/DFS 算法的若干疑问](https://labuladong.online/algo/essential-technique/backtrack-vs-dfs/) 会具体讲解,这里可以暂且忽略这个问题。 + 而所谓的前序遍历和后序遍历,他们只是两个很有用的时间点,我给你画张图你就明白了: -![](../pictures/backtracking/4.jpg) +![](https://labuladong.online/algo/images/backtracking/4.jpg) **前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行**。 -回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作: +回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确处理节点的属性,那么就要在这两个特殊时间点搞点动作: -![](../pictures/backtracking/5.jpg) +![](https://labuladong.online/algo/images/backtracking/5.jpg) 现在,你是否理解了回溯算法的这段核心框架? @@ -100,186 +154,170 @@ for 选择 in 选择列表: 下面,直接看全排列代码: ```java -List> res = new LinkedList<>(); - -/* 主函数,输入一组不重复的数字,返回它们的全排列 */ -List> permute(int[] nums) { - // 记录「路径」 - LinkedList track = new LinkedList<>(); - backtrack(nums, track); - return res; -} - -// 路径:记录在 track 中 -// 选择列表:nums 中不存在于 track 的那些元素 -// 结束条件:nums 中的元素全都在 track 中出现 -void backtrack(int[] nums, LinkedList track) { - // 触发结束条件 - if (track.size() == nums.length) { - res.add(new LinkedList(track)); - return; +class Solution { + List> res = new LinkedList<>(); + + // 主函数,输入一组不重复的数字,返回它们的全排列 + List> permute(int[] nums) { + // 记录「路径」 + LinkedList track = new LinkedList<>(); + // 「路径」中的元素会被标记为 true,避免重复使用 + boolean[] used = new boolean[nums.length]; + + backtrack(nums, track, used); + return res; } - - for (int i = 0; i < nums.length; i++) { - // 排除不合法的选择 - if (track.contains(nums[i])) - continue; - // 做选择 - track.add(nums[i]); - // 进入下一层决策树 - backtrack(nums, track); - // 取消选择 - track.removeLast(); + + // 路径:记录在 track 中 + // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + // 结束条件:nums 中的元素全都在 track 中出现 + void backtrack(int[] nums, LinkedList track, boolean[] used) { + // 触发结束条件 + if (track.size() == nums.length) { + res.add(new LinkedList(track)); + return; + } + + for (int i = 0; i < nums.length; i++) { + // 排除不合法的选择 + if (used[i]) { + // nums[i] 已经在 track 中,跳过 + continue; + } + // 做选择 + track.add(nums[i]); + used[i] = true; + // 进入下一层决策树 + backtrack(nums, track, used); + // 取消选择 + track.removeLast(); + used[i] = false; + } } } ``` -我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过 `nums` 和 `track` 推导出当前的选择列表: - -![](../pictures/backtracking/6.jpg) -至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,应为对链表使用 `contains` 方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,但是难理解一些,这里就不写了,有兴趣可以自行搜索一下。 +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
-但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。**这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高**。 -明白了全排列问题,就可以直接套回溯算法框架了,下面简单看看 N 皇后问题。 -### 二、N 皇后问题 +我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过 `used` 数组排除已经存在 `track` 中的元素,从而推导出当前的选择列表: -这个问题很经典了,简单解释一下:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。 +![](https://labuladong.online/algo/images/backtracking/6.jpg) -PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。 +至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是最高效的,你可能看到有的解法连 `used` 数组都不使用,通过交换元素达到目的。但是那种解法稍微难理解一些,我会在 [球盒模型:回溯算法两种穷举视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) 中介绍。 -这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。 +但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的,你最后肯定要穷举出 N! 种全排列结果。 -直接套用框架: +**这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高**。 -```cpp -vector> res; +## 最后总结 -/* 输入棋盘边长 n,返回所有合法的放置 */ -vector> solveNQueens(int n) { - // '.' 表示空,'Q' 表示皇后,初始化空棋盘。 - vector board(n, string(n, '.')); - backtrack(board, 0); - return res; -} +回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下: -// 路径:board 中小于 row 的那些行都已经成功放置了皇后 -// 选择列表:第 row 行的所有列都是放置皇后的选择 -// 结束条件:row 超过 board 的最后一行 -void backtrack(vector& board, int row) { - // 触发结束条件 - if (row == board.size()) { - res.push_back(board); - return; - } - - int n = board[row].size(); - for (int col = 0; col < n; col++) { - // 排除不合法选择 - if (!isValid(board, row, col)) - continue; - // 做选择 - board[row][col] = 'Q'; - // 进入下一行决策 - backtrack(board, row + 1); - // 撤销选择 - board[row][col] = '.'; - } -} +```python +def backtrack(...): + for 选择 in 选择列表: + 做选择 + backtrack(...) + 撤销选择 ``` -这部分主要代码,其实跟全排列问题差不多,`isValid` 函数的实现也很简单: +**写 `backtrack` 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集**。 -```cpp -/* 是否可以在 board[row][col] 放置皇后? */ -bool isValid(vector& board, int row, int col) { - int n = board.size(); - // 检查列是否有皇后互相冲突 - for (int i = 0; i < n; i++) { - if (board[i][col] == 'Q') - return false; - } - // 检查右上方是否有皇后互相冲突 - for (int i = row - 1, j = col + 1; - i >= 0 && j < n; i--, j++) { - if (board[i][j] == 'Q') - return false; - } - // 检查左上方是否有皇后互相冲突 - for (int i = row - 1, j = col - 1; - i >= 0 && j >= 0; i--, j--) { - if (board[i][j] == 'Q') - return false; - } - return true; -} -``` +其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」? -函数 `backtrack` 依然像个在决策树上游走的指针,通过 `row` 和 `col` 就可以表示函数遍历到的位置,通过 `isValid` 函数可以将不符合条件的情况剪枝: +动态规划和回溯算法底层都把问题抽象成了树的结构,但这两种算法在思路上是完全不同的。在 [二叉树心法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 你将看到动态规划和回溯算法更深层次的区别和联系。 -![](../pictures/backtracking/7.jpg) -如果直接给你这么一大段解法代码,可能是懵逼的。但是现在明白了回溯算法的框架套路,还有啥难理解的呢?无非是改改做选择的方式,排除不合法选择的方式而已,只要框架存于心,你面对的只剩下小问题了。 -当 `N = 8` 时,就是八皇后问题,数学大佬高斯穷尽一生都没有数清楚八皇后问题到底有几种可能的放置方法,但是我们的算法只需要一秒就可以算出来所有可能的结果。 -不过真的不怪高斯。这个问题的复杂度确实非常高,看看我们的决策树,虽然有 `isValid` 函数剪枝,但是最坏时间复杂度仍然是 O(N^(N+1)),而且无法优化。如果 `N = 10` 的时候,计算就已经很耗时了。 -**有的时候,我们并不想得到所有合法的答案,只想要一个答案,怎么办呢**?比如解数独的算法,找所有解法复杂度太高,只要找到一种解法就可以。 -其实特别简单,只要稍微修改一下回溯算法的代码即可: -```cpp -// 函数找到一个答案后就返回 true -bool backtrack(vector& board, int row) { - // 触发结束条件 - if (row == board.size()) { - res.push_back(board); - return true; - } - ... - for (int col = 0; col < n; col++) { - ... - board[row][col] = 'Q'; +
+
+引用本文的文章 - if (backtrack(board, row + 1)) - return true; - - board[row][col] = '.'; - } + - [Trie/字典树/前缀树代码实现](https://labuladong.online/algo/data-structure/trie-implement/) + - [base case 和备忘录的初始值怎么定?](https://labuladong.online/algo/dynamic-programming/memo-fundamental/) + - [【强化练习】同时运用两种思维解题](https://labuladong.online/algo/problem-set/binary-tree-combine-two-view/) + - [【强化练习】回溯算法经典习题 I](https://labuladong.online/algo/problem-set/backtrack-i/) + - [【强化练习】回溯算法经典习题 II](https://labuladong.online/algo/problem-set/backtrack-ii/) + - [【强化练习】回溯算法经典习题 III](https://labuladong.online/algo/problem-set/backtrack-iii/) + - [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) + - [二叉树基础及常见类型](https://labuladong.online/algo/data-structure-basic/binary-tree-basic/) + - [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + - [分治算法解题套路框架](https://labuladong.online/algo/essential-technique/divide-and-conquer/) + - [动态规划和回溯算法的思维转换](https://labuladong.online/algo/dynamic-programming/word-break/) + - [回溯算法实践:括号生成](https://labuladong.online/algo/practice-in-action/generate-parentheses/) + - [回溯算法实践:数独和 N 皇后问题](https://labuladong.online/algo/practice-in-action/sudoku-nqueue/) + - [回溯算法实践:集合划分](https://labuladong.online/algo/practice-in-action/partition-to-k-equal-sum-subsets/) + - [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [球盒模型:回溯算法穷举的两种视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) + - [算法学习和心流体验](https://labuladong.online/algo/fname.html?fname=心流) + - [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + - [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + - [经典动态规划:戳气球](https://labuladong.online/algo/dynamic-programming/burst-balloons/) + - [背包问题的变体:目标和](https://labuladong.online/algo/dynamic-programming/target-sum/) + - [解答回溯算法/DFS算法的若干疑问](https://labuladong.online/algo/essential-technique/backtrack-vs-dfs/) - return false; -} -``` +

-这样修改后,只要找到一个答案,for 循环的后续递归穷举都会被阻断。也许你可以在 N 皇后问题的代码框架上,稍加修改,写一个解数独的算法? -### 三、最后总结 -回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下: -```python -def backtrack(...): - for 选择 in 选择列表: - 做选择 - backtrack(...) - 撤销选择 -``` +
+
+引用本文的题目 -**写 `backtrack` 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集**。 +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: -其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」? +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [111. Minimum Depth of Binary Tree](https://leetcode.com/problems/minimum-depth-of-binary-tree/?show=1) | [111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/?show=1) | 🟢 | +| [112. Path Sum](https://leetcode.com/problems/path-sum/?show=1) | [112. 路径总和](https://leetcode.cn/problems/path-sum/?show=1) | 🟢 | +| [113. Path Sum II](https://leetcode.com/problems/path-sum-ii/?show=1) | [113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/?show=1) | 🟠 | +| [131. Palindrome Partitioning](https://leetcode.com/problems/palindrome-partitioning/?show=1) | [131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/?show=1) | 🟠 | +| [140. Word Break II](https://leetcode.com/problems/word-break-ii/?show=1) | [140. 单词拆分 II](https://leetcode.cn/problems/word-break-ii/?show=1) | 🔴 | +| [1593. Split a String Into the Max Number of Unique Substrings](https://leetcode.com/problems/split-a-string-into-the-max-number-of-unique-substrings/?show=1) | [1593. 拆分字符串使唯一子字符串的数目最大](https://leetcode.cn/problems/split-a-string-into-the-max-number-of-unique-substrings/?show=1) | 🟠 | +| [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number/?show=1) | [17. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/?show=1) | 🟠 | +| [22. Generate Parentheses](https://leetcode.com/problems/generate-parentheses/?show=1) | [22. 括号生成](https://leetcode.cn/problems/generate-parentheses/?show=1) | 🟠 | +| [301. Remove Invalid Parentheses](https://leetcode.com/problems/remove-invalid-parentheses/?show=1) | [301. 删除无效的括号](https://leetcode.cn/problems/remove-invalid-parentheses/?show=1) | 🔴 | +| [332. Reconstruct Itinerary](https://leetcode.com/problems/reconstruct-itinerary/?show=1) | [332. 重新安排行程](https://leetcode.cn/problems/reconstruct-itinerary/?show=1) | 🔴 | +| [39. Combination Sum](https://leetcode.com/problems/combination-sum/?show=1) | [39. 组合总和](https://leetcode.cn/problems/combination-sum/?show=1) | 🟠 | +| [51. N-Queens](https://leetcode.com/problems/n-queens/?show=1) | [51. N 皇后](https://leetcode.cn/problems/n-queens/?show=1) | 🔴 | +| [638. Shopping Offers](https://leetcode.com/problems/shopping-offers/?show=1) | [638. 大礼包](https://leetcode.cn/problems/shopping-offers/?show=1) | 🟠 | +| [698. Partition to K Equal Sum Subsets](https://leetcode.com/problems/partition-to-k-equal-sum-subsets/?show=1) | [698. 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/?show=1) | 🟠 | +| [77. Combinations](https://leetcode.com/problems/combinations/?show=1) | [77. 组合](https://leetcode.cn/problems/combinations/?show=1) | 🟠 | +| [78. Subsets](https://leetcode.com/problems/subsets/?show=1) | [78. 子集](https://leetcode.cn/problems/subsets/?show=1) | 🟠 | +| [784. Letter Case Permutation](https://leetcode.com/problems/letter-case-permutation/?show=1) | [784. 字母大小写全排列](https://leetcode.cn/problems/letter-case-permutation/?show=1) | 🟠 | +| [89. Gray Code](https://leetcode.com/problems/gray-code/?show=1) | [89. 格雷编码](https://leetcode.cn/problems/gray-code/?show=1) | 🟠 | +| [93. Restore IP Addresses](https://leetcode.com/problems/restore-ip-addresses/?show=1) | [93. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/?show=1) | 🟠 | +| - | [剑指 Offer 34. 二叉树中和为某一值的路径](https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/?show=1) | 🟠 | +| - | [剑指 Offer II 079. 所有子集](https://leetcode.cn/problems/TVdhkn/?show=1) | 🟠 | +| - | [剑指 Offer II 080. 含有 k 个元素的组合](https://leetcode.cn/problems/uUsW3B/?show=1) | 🟠 | +| - | [剑指 Offer II 081. 允许重复选择元素的组合](https://leetcode.cn/problems/Ygoe9J/?show=1) | 🟠 | +| - | [剑指 Offer II 083. 没有重复元素集合的全排列](https://leetcode.cn/problems/VvJkup/?show=1) | 🟠 | +| - | [剑指 Offer II 085. 生成匹配的括号](https://leetcode.cn/problems/IDBivT/?show=1) | 🟠 | + +
+
-某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:动态规划答疑篇](../动态规划系列/最优子结构.md) -[下一篇:二分查找解题框架](../算法思维系列/二分查找详解.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" index 9638127110..93166895cb 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" @@ -1,14 +1,34 @@ -# 字符串乘法 +# 字符串乘法计算 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [43. Multiply Strings](https://leetcode.com/problems/multiply-strings/) | [43. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) | 🟠 | + +**-----------** + + 对于比较小的数字,做运算可以直接使用编程语言提供的运算符,但是如果相乘的两个因数非常大,语言提供的数据类型可能就会溢出。一种替代方案就是,运算数以字符串的形式输入,然后模仿我们小学学习的乘法算术过程计算出结果,并且也用字符串表示。 -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/title.png) +看下力扣第 43 题「字符串相乘」: + + 需要注意的是,`num1` 和 `num2` 可以非常长,所以不可以把他们直接转成整型然后运算,唯一的思路就是模仿我们手算乘法。 比如说我们手算 `123 × 45`,应该会这样计算: -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/1.jpg) +![](https://labuladong.online/algo/images/string-multiply/1.jpg) 计算 `123 × 5`,再计算 `123 × 4`,最后错一位相加。这个流程恐怕小学生都可以熟练完成,但是你是否能**把这个运算过程进一步机械化**,写成一套算法指令让没有任何智商的计算机来执行呢? @@ -16,50 +36,53 @@ 首先,我们这种手算方式还是太「高级」了,我们要再「低级」一点,`123 × 5` 和 `123 × 4` 的过程还可以进一步分解,最后再相加: -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/2.jpg) +![](https://labuladong.online/algo/images/string-multiply/2.jpg) 现在 `123` 并不大,如果是个很大的数字的话,是无法直接计算乘积的。我们可以用一个数组在底下接收相加结果: -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/3.jpg) +![](https://labuladong.online/algo/images/string-multiply/3.jpg) -整个计算过程大概是这样,**有两个指针 `i,j` 在 `num1` 和 `num2` 上游走,计算乘积,同时将乘积叠加到 `res` 的正确位置**: +整个计算过程大概是这样,**有两个指针 `i,j` 在 `num1` 和 `num2` 上游走,计算乘积,同时将乘积叠加到 `res` 的正确位置**,如下 GIF 图所示: -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/4.gif) +![](https://labuladong.online/algo/images/string-multiply/4.gif) 现在还有一个关键问题,如何将乘积叠加到 `res` 的正确位置,或者说,如何通过 `i,j` 计算 `res` 的对应索引呢? 其实,细心观察之后就发现,**`num1[i]` 和 `num2[j]` 的乘积对应的就是 `res[i+j]` 和 `res[i+j+1]` 这两个位置**。 -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/6.jpg) +![](https://labuladong.online/algo/images/string-multiply/6.jpg) 明白了这一点,就可以用代码模仿出这个计算过程了: ```java -string multiply(string num1, string num2) { - int m = num1.size(), n = num2.size(); - // 结果最多为 m + n 位数 - vector res(m + n, 0); - // 从个位数开始逐位相乘 - for (int i = m - 1; i >= 0; i--) - for (int j = n - 1; j >= 0; j--) { - int mul = (num1[i]-'0') * (num2[j]-'0'); - // 乘积在 res 对应的索引位置 - int p1 = i + j, p2 = i + j + 1; - // 叠加到 res 上 - int sum = mul + res[p2]; - res[p2] = sum % 10; - res[p1] += sum / 10; +class Solution { + public String multiply(String num1, String num2) { + int m = num1.length(), n = num2.length(); + // 结果最多为 m + n 位数 + int[] res = new int[m + n]; + // 从个位数开始逐位相乘 + for (int i = m - 1; i >= 0; i--) { + for (int j = n - 1; j >= 0; j--) { + int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0'); + // 乘积在 res 对应的索引位置 + int p1 = i + j, p2 = i + j + 1; + // 叠加到 res 上 + int sum = mul + res[p2]; + res[p2] = sum % 10; + res[p1] += sum / 10; + } } - // 结果前缀可能存的 0(未使用的位) - int i = 0; - while (i < res.size() && res[i] == 0) - i++; - // 将计算结果转化成字符串 - string str; - for (; i < res.size(); i++) - str.push_back('0' + res[i]); - - return str.size() == 0 ? "0" : str; + // 结果前缀可能存的 0(未使用的位) + int i = 0; + while (i < res.length && res[i] == 0) + i++; + // 将计算结果转化成字符串 + StringBuilder str = new StringBuilder(); + for (; i < res.length; i++) + str.append(res[i]); + + return str.length() == 0 ? "0" : str.toString(); + } } ``` @@ -71,13 +94,16 @@ string multiply(string num1, string num2) { 也许算法就是一种**寻找思维定式的思维**吧,希望本文对你有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:前缀和技巧](../算法思维系列/前缀和技巧.md) -[下一篇:FloodFill算法详解及应用](../算法思维系列/FloodFill算法详解及应用.md) -[目录](../README.md#目录) \ No newline at end of file + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" index 7b7fcfd594..711687cf94 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" @@ -1,8 +1,27 @@ # 学习数据结构和算法的框架思维 -这是好久之前的一篇文章「学习数据结构和算法的框架思维」的修订版。之前那篇文章收到广泛好评,没看过也没关系,这篇文章会涵盖之前的所有内容,并且会举很多代码的实例,教你如何使用框架思维。 +

+GitHub + + + +

-首先,这里讲的都是普通的数据结构,咱不是搞算法竞赛的,野路子出生,我只会解决常规的问题。另外,以下是我个人的经验的总结,没有哪本算法书会写这些东西,所以请读者试着理解我的角度,别纠结于细节问题,因为这篇文章就是希望对数据结构和算法建立一个框架性的认识。 +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +**-----------** + +> tip:本文有视频版:[学习数据结构和算法的框架思维](https://www.bilibili.com/video/BV1EN4y1M79p/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +这是好久之前的一篇文章 [学习数据结构和算法的框架思维](https://mp.weixin.qq.com/s/gE-5KMi4bBvJovdsQXIKgw) 的修订版。之前那篇文章收到广泛好评,没看过也没关系,这篇文章会涵盖之前的所有内容,并且会举很多代码的实例,教你如何使用框架思维。 + +首先,这里讲的都是普通的数据结构,咱不是搞算法竞赛的,咱的目的是迅速提升算法能力,培养算法思维,真没必要整太偏太怪的题目。另外,以下是我个人的经验的总结,没有哪本算法书会写这些东西,所以请读者试着理解我的角度,别纠结于细节问题,因为这篇文章就是希望帮你对数据结构和算法建立一个框架性的认识。 从整体到细节,自顶向下,从抽象到具体的框架思维是通用的,不只是学习数据结构和算法,学习其他任何知识都是高效的。 @@ -30,7 +49,6 @@ **链表**因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1)。但是正因为存储空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问;而且由于每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。 - ### 二、数据结构的基本操作 对于任何数据结构,其基本操作无非遍历 + 访问,再具体一点就是:增删查改。 @@ -43,6 +61,7 @@ 数组遍历框架,典型的线性迭代结构: + ```java void traverse(int[] arr) { for (int i = 0; i < arr.length; i++) { @@ -53,6 +72,7 @@ void traverse(int[] arr) { 链表遍历框架,兼具迭代和递归结构: + ```java /* 基本的单链表节点 */ class ListNode { @@ -68,12 +88,13 @@ void traverse(ListNode head) { void traverse(ListNode head) { // 递归访问 head.val - traverse(head.next) + traverse(head.next); } ``` 二叉树遍历框架,典型的非线性递归遍历结构: + ```java /* 基本的二叉树节点 */ class TreeNode { @@ -82,8 +103,8 @@ class TreeNode { } void traverse(TreeNode root) { - traverse(root.left) - traverse(root.right) + traverse(root.left); + traverse(root.right); } ``` @@ -91,6 +112,7 @@ void traverse(TreeNode root) { 二叉树框架可以扩展为 N 叉树的遍历框架: + ```java /* 基本的 N 叉树节点 */ class TreeNode { @@ -100,139 +122,188 @@ class TreeNode { void traverse(TreeNode root) { for (TreeNode child : root.children) - traverse(child) + traverse(child); } ``` -N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵树的结合体。你说图是可能出现环的?这个很好办,用个布尔数组 visited 做标记就行了,这里就不写代码了。 +`N` 叉树的遍历又可以扩展为图的遍历,因为图就是好几 `N` 叉棵树的结合体。你说图是可能出现环的?这个很好办,用个布尔数组 `visited` 做标记就行了,这里就不写代码了。 -**所谓框架,就是套路。不管增删查改,这些代码都是永远无法脱离的结构,你可以把这个结构作为大纲,根据具体问题在框架上添加代码就行了,下面会具体举例**。 +**所谓框架,就是套路。不管增删查改,这些代码都是永远无法脱离的结构**,你可以把这个结构作为大纲,根据具体问题在框架上添加代码就行了,下面会具体举例。 ### 三、算法刷题指南 -首先要明确的是,**数据结构是工具,算法是通过合适的工具解决特定问题的方法**。也就是说,学习算法之前,最起码得了解那些常用的数据结构,了解它们的特性和缺陷。 +首先要明确的是,数据结构是工具,算法是通过合适的工具解决特定问题的方法。也就是说,学习算法之前,最起码得了解那些常用的数据结构,了解它们的特性和缺陷。 + +所以我建议的刷题顺序是: + +**1、先学习像数组、链表这种基本数据结构的常用算法**,比如单链表翻转,前缀和数组,二分搜索等。 + +因为这些算法属于会者不难难者不会的类型,难度不大,学习它们不会花费太多时间。而且这些小而美的算法经常让你大呼精妙,能够有效培养你对算法的兴趣。 + +**2、学会基础算法之后,不要急着上来就刷回溯算法、动态规划这类笔试常考题,而应该先刷二叉树,先刷二叉树,先刷二叉树**,重要的事情说三遍。 + +::: tip 提示 -那么该如何在 LeetCode 刷题呢?之前的文章[算法学习之路](算法学习之路.md)写过一些,什么按标签刷,坚持下去云云。现在距那篇文章已经过去将近一年了,我不说那些不痛不痒的话,直接说具体的建议: +力扣上有专门的二叉树题目分类: -**先刷二叉树,先刷二叉树,先刷二叉树**! +[https://leetcode.cn/tag/binary-tree/](https://leetcode.cn/tag/binary-tree/) -这是我这刷题一年的亲身体会,下图是去年十月份的提交截图: +::: -![](../pictures/others/leetcode.jpeg) +这是我这刷题多年的亲身体会,下图是我刚开始学算法的提交截图: -公众号文章的阅读数据显示,大部分人对数据结构相关的算法文章不感兴趣,而是更关心动规回溯分治等等技巧。为什么要先刷二叉树呢,**因为二叉树是最容易培养框架思维的,而且大部分算法技巧,本质上都是树的遍历问题**。 +![](https://labuladong.online/algo/images/others/leetcode.jpeg) -刷二叉树看到题目没思路?根据很多读者的问题,其实大家不是没思路,只是没有理解我们说的「框架」是什么。**不要小看这几行破代码,几乎所有二叉树的题目都是一套这个框架就出来了**。 +公众号文章的阅读数据显示,大部分人对数据结构相关的算法文章不感兴趣,而是更关心动规回溯分治等等技巧。为什么要先刷二叉树呢,**因为二叉树是最容易培养框架思维的,而且所有的递归算法技巧,本质上都是树的遍历问题**。 +刷二叉树看到题目没思路?根据很多读者的问题,其实大家不是没思路,只是没有理解我们说的「框架」是什么。 + +**不要小看这几行破代码,几乎所有二叉树的题目都是一套这个框架就出来了**: + + ```java void traverse(TreeNode root) { - // 前序遍历 - traverse(root.left) - // 中序遍历 - traverse(root.right) - // 后序遍历 + // 前序位置 + traverse(root.left); + // 中序位置 + traverse(root.right); + // 后序位置 } ``` 比如说我随便拿几道题的解法出来,不用管具体的代码逻辑,只要看看框架在其中是如何发挥作用的就行。 -LeetCode 124 题,难度 Hard,让你求二叉树中最大路径和,主要代码如下: +力扣第 124 题,难度困难,让你求二叉树中最大路径和,主要代码如下: -```cpp -int ans = INT_MIN; -int oneSideMax(TreeNode* root) { - if (root == nullptr) return 0; - int left = max(0, oneSideMax(root->left)); - int right = max(0, oneSideMax(root->right)); - ans = max(ans, left + right + root->val); - return max(left, right) + root->val; + +```java +int res = Integer.MIN_VALUE; +int oneSideMax(TreeNode root) { + if (root == null) return 0; + int left = max(0, oneSideMax(root.left)); + int right = max(0, oneSideMax(root.right)); + // 后序位置 + res = Math.max(res, left + right + root.val); + return Math.max(left, right) + root.val; } ``` -你看,这就是个后序遍历嘛。 +注意递归函数的位置,这就是个后序遍历嘛,无非就是把 `traverse` 函数名字改成 `oneSideMax` 了。 -LeetCode 105 题,难度 Medium,让你根据前序遍历和中序遍历的结果还原一棵二叉树,很经典的问题吧,主要代码如下: +力扣第 105 题,难度中等,让你根据前序遍历和中序遍历的结果还原一棵二叉树,很经典的问题吧,主要代码如下: + ```java -TreeNode buildTree(int[] preorder, int preStart, int preEnd, - int[] inorder, int inStart, int inEnd, Map inMap) { - - if(preStart > preEnd || inStart > inEnd) return null; - - TreeNode root = new TreeNode(preorder[preStart]); - int inRoot = inMap.get(root.val); - int numsLeft = inRoot - inStart; - - root.left = buildTree(preorder, preStart + 1, preStart + numsLeft, - inorder, inStart, inRoot - 1, inMap); - root.right = buildTree(preorder, preStart + numsLeft + 1, preEnd, - inorder, inRoot + 1, inEnd, inMap); +TreeNode build(int[] preorder, int preStart, int preEnd, + int[] inorder, int inStart, int inEnd) { + // 前序位置,寻找左右子树的索引 + if (preStart > preEnd) { + return null; + } + int rootVal = preorder[preStart]; + int index = 0; + for (int i = inStart; i <= inEnd; i++) { + if (inorder[i] == rootVal) { + index = i; + break; + } + } + int leftSize = index - inStart; + TreeNode root = new TreeNode(rootVal); + + // 递归构造左右子树 + root.left = build(preorder, preStart + 1, preStart + leftSize, + inorder, inStart, index - 1); + root.right = build(preorder, preStart + leftSize + 1, preEnd, + inorder, index + 1, inEnd); return root; } ``` -不要看这个函数的参数很多,只是为了控制数组索引而已,本质上该算法也就是一个前序遍历。 +不要看这个函数的参数很多,只是为了控制数组索引而已。注意找递归函数 `build` 的位置,本质上该算法也就是一个前序遍历,因为它在前序遍历的位置加了一坨代码逻辑。 -LeetCode 99 题,难度 Hard,恢复一棵 BST,主要代码如下: +力扣第 230 题,难度中等,寻找二叉搜索树中的第 `k` 小的元素,主要代码如下: -```cpp -void traverse(TreeNode* node) { - if (!node) return; - traverse(node->left); - if (node->val < prev->val) { - s = (s == NULL) ? prev : s; - t = node; + +```java +int res = 0; +int rank = 0; +void traverse(TreeNode root, int k) { + if (root == null) { + return; + } + traverse(root.left, k); + /* 中序遍历代码位置 */ + rank++; + if (k == rank) { + res = root.val; + return; } - prev = node; - traverse(node->right); + /*****************/ + traverse(root.right, k); } ``` 这不就是个中序遍历嘛,对于一棵 BST 中序遍历意味着什么,应该不需要解释了吧。 -你看,Hard 难度的题目不过如此,而且还这么有规律可循,只要把框架写出来,然后往相应的位置加东西就行了,这不就是思路吗。 +你看,二叉树的题目不过如此,只要把框架写出来,然后往相应的位置加代码就行了,这不就是思路吗。 对于一个理解二叉树的人来说,刷一道二叉树的题目花不了多长时间。那么如果你对刷题无从下手或者有畏惧心理,不妨从二叉树下手,前 10 道也许有点难受;结合框架再做 20 道,也许你就有点自己的理解了;刷完整个专题,再去做什么回溯动规分治专题,**你就会发现只要涉及递归的问题,都是树的问题**。 +::: tip + +本站的 [二叉树专项练习章节](https://labuladong.online/algo/problem-set/binary-tree-traverse-1/) 按照固定的公式和思维模式讲解了 150 道二叉树题目,可以手把手带你刷完二叉树分类的题目,迅速掌握递归思维。 + +::: + 再举例吧,说几道我们之前文章写过的问题。 -[动态规划详解](../动态规划系列/动态规划详解进阶.md)说过凑零钱问题,暴力解法就是遍历一棵 N 叉树: +[动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/)说过凑零钱问题,暴力解法就是遍历一棵 N 叉树: -![](../pictures/动态规划详解进阶/5.jpg) +![](https://labuladong.online/algo/images/动态规划详解进阶/5.jpg) -```python -def coinChange(coins: List[int], amount: int): - - def dp(n): - if n == 0: return 0 - if n < 0: return -1 - - res = float('INF') - for coin in coins: - subproblem = dp(n - coin) - # 子问题无解,跳过 - if subproblem == -1: continue - res = min(res, 1 + subproblem) - return res if res != float('INF') else -1 - - return dp(amount) + +```java +int dp(int[] coins, int amount) { + // base case + if (amount == 0) return 0; + if (amount < 0) return -1; + + int res = Integer.MAX_VALUE; + for (int coin : coins) { + int subProblem = dp(coins, amount - coin); + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); + } + return res == Integer.MAX_VALUE ? -1 : res; +} ``` 这么多代码看不懂咋办?直接提取出框架,就能看出核心思路了: + ```python # 不过是一个 N 叉树的遍历问题而已 -def dp(n): - for coin in coins: - dp(n - coin) +int dp(int amount) { + for (int coin : coins) { + dp(amount - coin); + } +} ``` 其实很多动态规划问题就是在遍历一棵树,你如果对树的遍历操作烂熟于心,起码知道怎么把思路转化成代码,也知道如何提取别人解法的核心思路。 -再看看回溯算法,前文[回溯算法详解](回溯算法详解修订版.md)干脆直接说了,回溯算法就是个 N 叉树的前后序遍历问题,没有例外。 +再看看回溯算法,前文 [回溯算法详解](https://labuladong.online/algo/essential-technique/backtrack-framework/) 干脆直接说了,回溯算法就是个 N 叉树的前后序遍历问题,没有例外。 + +比如全排列问题吧,本质上全排列就是在遍历下面这棵树,到叶子节点的路径就是一个全排列: -比如 N 皇后问题吧,主要代码如下: +![](https://labuladong.online/algo/images/backtracking/1.jpg) +全排列算法的主要代码如下: + + ```java void backtrack(int[] nums, LinkedList track) { if (track.size() == nums.length) { @@ -248,7 +319,13 @@ void backtrack(int[] nums, LinkedList track) { backtrack(nums, track); track.removeLast(); } +} +``` + +看不懂?没关系,把其中的递归部分抽取出来: + +```java /* 提取出 N 叉树遍历框架 */ void backtrack(int[] nums, LinkedList track) { for (int i = 0; i < nums.length; i++) { @@ -256,11 +333,11 @@ void backtrack(int[] nums, LinkedList track) { } ``` -N 叉树的遍历框架,找出来了把~你说,树这种结构重不重要? +N 叉树的遍历框架,找出来了吧?你说,树这种结构重不重要? -**综上,对于畏惧算法的朋友来说,可以先刷树的相关题目,试着从框架上看问题,而不要纠结于细节问题**。 +**综上,对于畏惧算法的同学来说,可以先刷树的相关题目,试着从框架上看问题,而不要纠结于细节问题**。 -纠结细节问题,就比如纠结 i 到底应该加到 n 还是加到 n - 1,这个数组的大小到底应该开 n 还是 n + 1 ? +纠结细节问题,就比如纠结 `i` 到底应该加到 `n` 还是加到 `n - 1`,这个数组的大小到底应该开 `n` 还是 `n + 1`? 从框架上看问题,就是像我们这样基于框架进行抽取和扩展,既可以在看别人解法时快速理解核心逻辑,也有助于找到我们自己写解法时的思路方向。 @@ -268,23 +345,74 @@ N 叉树的遍历框架,找出来了把~你说,树这种结构重不重要 但是,你要是心中没有框架,那么你根本无法解题,给了你答案,你也不会发现这就是个树的遍历问题。 -这种思维是很重要的,[动态规划详解](../动态规划系列/动态规划详解进阶.md)中总结的找状态转移方程的几步流程,有时候按照流程写出解法,说实话我自己都不知道为啥是对的,反正它就是对了。。。 - -**这就是框架的力量,能够保证你在快睡着的时候,依然能写出正确的程序;就算你啥都不会,都能比别人高一个级别。** +这种思维是很重要的,[动态规划详解](https://labuladong.online/algo/essential-technique/dynamic-programming-framework/) 中总结的找状态转移方程的几步流程,有时候按照流程写出解法,可能自己都不知道为啥是对的,反正它就是对了。。。 +**这就是框架的力量,能够保证你在快睡着的时候,依然能写出正确的程序;就算你啥都不会,都能比别人高一个级别**。 -### 四、总结几句 +本文最后,总结一下吧: 数据结构的基本存储方式就是链式和顺序两种,基本操作就是增删查改,遍历方式无非迭代和递归。 -刷算法题建议从「树」分类开始刷,结合框架思维,把这几十道题刷完,对于树结构的理解应该就到位了。这时候去看回溯、动规、分治等算法专题,对思路的理解可能会更加深刻一些。 +学完基本算法之后,建议从「二叉树」系列问题开始刷,结合框架思维,把树结构理解到位,然后再去看回溯、动规、分治等算法专题,对思路的理解就会更加深刻。 + + + + + +
+
+引用本文的文章 + + - [Dijkstra 算法模板及应用](https://labuladong.online/algo/data-structure/dijkstra/) + - [一文秒杀所有岛屿题目](https://labuladong.online/algo/frequency-interview/island-dfs-summary/) + - [东哥带你刷二叉树(序列化篇)](https://labuladong.online/algo/data-structure/serialize-and-deserialize-binary-tree/) + - [东哥带你刷二叉树(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + - [二分图判定算法](https://labuladong.online/algo/data-structure/bipartite-graph/) + - [二叉堆的基本原理](https://labuladong.online/algo/data-structure-basic/binary-heap-basic/) + - [前缀树算法模板秒杀五道算法题](https://labuladong.online/algo/data-structure/trie/) + - [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.online/algo/essential-technique/permutation-combination-subset-all-in-one/) + - [回溯算法解题套路框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) + - [图论基础及遍历算法](https://labuladong.online/algo/data-structure/graph-traverse/) + - [如何 K 个一组反转链表](https://labuladong.online/algo/data-structure/reverse-nodes-in-k-group/) + - [如何判断回文链表](https://labuladong.online/algo/data-structure/palindrome-linked-list/) + - [归并排序详解及应用](https://labuladong.online/algo/practice-in-action/merge-sort/) + - [我的刷题心得:算法的本质](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [数组(顺序存储)基本原理](https://labuladong.online/algo/data-structure-basic/array-basic/) + - [浅谈存储系统:LSM 树设计原理](https://labuladong.online/algo/other-skills/lsm-tree/) + - [环检测及拓扑排序算法](https://labuladong.online/algo/data-structure/topological-sort/) + - [用栈模拟递归迭代遍历二叉树](https://labuladong.online/algo/data-structure/iterative-traversal-binary-tree/) + - [用链表实现队列/栈](https://labuladong.online/algo/data-structure-basic/linked-queue-stack/) + - [目标和问题:背包问题的变体](https://labuladong.online/algo/dynamic-programming/target-sum/) + - [算法学习和心流体验](https://labuladong.online/algo/other-skills/hert-flow/) + - [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + - [题目不让我干什么,我偏要干什么](https://labuladong.online/algo/data-structure/flatten-nested-list-iterator/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | +| :----: | :----: | +| [341. Flatten Nested List Iterator](https://leetcode.com/problems/flatten-nested-list-iterator/?show=1) | [341. 扁平化嵌套列表迭代器](https://leetcode.cn/problems/flatten-nested-list-iterator/?show=1) | +| [589. N-ary Tree Preorder Traversal](https://leetcode.com/problems/n-ary-tree-preorder-traversal/?show=1) | [589. N 叉树的前序遍历](https://leetcode.cn/problems/n-ary-tree-preorder-traversal/?show=1) | +| [590. N-ary Tree Postorder Traversal](https://leetcode.com/problems/n-ary-tree-postorder-traversal/?show=1) | [590. N 叉树的后序遍历](https://leetcode.cn/problems/n-ary-tree-postorder-traversal/?show=1) | + +
+
+ -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:最长公共子序列](../动态规划系列/最长公共子序列.md) +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: -[下一篇:学习数据结构和算法读什么书](../算法思维系列/为什么推荐算法4.md) +![](https://labuladong.online/algo/images/souyisou2.png) -[目录](../README.md#目录) \ No newline at end of file +======其他语言代码====== \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" new file mode 100644 index 0000000000..97804f6b5f --- /dev/null +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" @@ -0,0 +1,325 @@ +# 小而美的算法技巧:差分数组 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1094. Car Pooling](https://leetcode.com/problems/car-pooling/) | [1094. 拼车](https://leetcode.cn/problems/car-pooling/) | 🟠 | +| [1109. Corporate Flight Bookings](https://leetcode.com/problems/corporate-flight-bookings/) | [1109. 航班预订统计](https://leetcode.cn/problems/corporate-flight-bookings/) | 🟠 | +| [370. Range Addition](https://leetcode.com/problems/range-addition/)🔒 | [370. 区间加法](https://leetcode.cn/problems/range-addition/)🔒 | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) +> - [前缀和技巧](https://labuladong.online/algo/data-structure/prefix-sum/) + +[前缀和技巧](https://labuladong.online/algo/data-structure/prefix-sum/) 主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和,核心代码就是下面这段: + +```java +class PrefixSum { + // 前缀和数组 + private int[] preSum; + + // 输入一个数组,构造前缀和 + public PrefixSum(int[] nums) { + // preSum[0] = 0,便于计算累加和 + preSum = new int[nums.length + 1]; + // 计算 nums 的累加和 + for (int i = 1; i < preSum.length; i++) { + preSum[i] = preSum[i - 1] + nums[i - 1]; + } + } + + // 查询闭区间 [left, right] 的累加和 + public int sumRange(int left, int right) { + return preSum[right + 1] - preSum[left]; + } +} +``` + +![](https://labuladong.online/algo/images/difference/1.jpeg) + + + +`preSum[i]` 就代表着 `nums[0..i-1]` 所有元素的累加和,如果我们想求区间 `nums[i..j]` 的累加和,只要计算 `preSum[j+1] - preSum[i]` 即可,而不需要遍历整个区间求和。 + +本文讲一个和前缀和思想非常类似的算法技巧「差分数组」,**差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减**。 + +比如说,我给你输入一个数组 `nums`,然后又要求给区间 `nums[2..6]` 全部加 1,再给 `nums[3..9]` 全部减 3,再给 `nums[0..4]` 全部加 2,再给... + +一通操作猛如虎,然后问你,最后 `nums` 数组的值是什么? + +常规的思路很容易,你让我给区间 `nums[i..j]` 加上 `val`,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的时间复杂度是 $O(N)$,由于这个场景下对 `nums` 的修改非常频繁,所以效率会很低下。 + +这里就需要差分数组的技巧,类似前缀和技巧构造的 `preSum` 数组,我们先对 `nums` 数组构造一个 `diff` 差分数组,**`diff[i]` 就是 `nums[i]` 和 `nums[i-1]` 之差**: + +```java +int[] diff = new int[nums.length]; +// 构造差分数组 +diff[0] = nums[0]; +for (int i = 1; i < nums.length; i++) { + diff[i] = nums[i] - nums[i - 1]; +} +``` + +![](https://labuladong.online/algo/images/difference/2.jpeg) + + + +通过这个 `diff` 差分数组是可以反推出原始数组 `nums` 的,代码逻辑如下: + +```java +int[] res = new int[diff.length]; +// 根据差分数组构造结果数组 +res[0] = diff[0]; +for (int i = 1; i < diff.length; i++) { + res[i] = res[i - 1] + diff[i]; +} +``` + +**这样构造差分数组 `diff`,就可以快速进行区间增减的操作**,如果你想对区间 `nums[i..j]` 的元素全部加 3,那么只需要让 `diff[i] += 3`,然后再让 `diff[j+1] -= 3` 即可: + +![](https://labuladong.online/algo/images/difference/3.jpeg) + +**原理很简单,回想 `diff` 数组反推 `nums` 数组的过程,`diff[i] += 3` 意味着给 `nums[i..]` 所有的元素都加了 3,然后 `diff[j+1] -= 3` 又意味着对于 `nums[j+1..]` 所有元素再减 3,那综合起来,是不是就是对 `nums[i..j]` 中的所有元素都加 3 了**? + +只要花费 O(1) 的时间修改 `diff` 数组,就相当于给 `nums` 的整个区间做了修改。多次修改 `diff`,然后通过 `diff` 数组反推,即可得到 `nums` 修改后的结果。 + +现在我们把差分数组抽象成一个类,包含 `increment` 方法和 `result` 方法: + +```java +// 差分数组工具类 +class Difference { + // 差分数组 + private int[] diff; + + // 输入一个初始数组,区间操作将在这个数组上进行 + public Difference(int[] nums) { + assert nums.length > 0; + diff = new int[nums.length]; + // 根据初始数组构造差分数组 + diff[0] = nums[0]; + for (int i = 1; i < nums.length; i++) { + diff[i] = nums[i] - nums[i - 1]; + } + } + + // 给闭区间 [i, j] 增加 val(可以是负数) + public void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.length) { + diff[j + 1] -= val; + } + } + + // 返回结果数组 + public int[] result() { + int[] res = new int[diff.length]; + // 根据差分数组构造结果数组 + res[0] = diff[0]; + for (int i = 1; i < diff.length; i++) { + res[i] = res[i - 1] + diff[i]; + } + return res; + } +} +``` + +这里注意一下 `increment` 方法中的 if 语句: + +```java +void increment(int i, int j, int val) { + diff[i] += val; + if (j + 1 < diff.length) { + diff[j + 1] -= val; + } +} +``` + +当 `j+1 >= diff.length` 时,说明是对 `nums[i]` 及以后的整个数组都进行修改,那么就不需要再给 `diff` 数组减 `val` 了。 + + + +你可以点开下面的可视化面板,多次点击 diff[i] = nums[i] - nums[i - 1] 这行代码就可以看到 `diff` 数组的构建过程,再多次点击 df.increment 这行代码可以看到 `diff` 数组的操作: + + + +## 算法实践 + +首先,力扣第 370 题「区间加法」 就直接考察了差分数组技巧: + + + +那么我们直接复用刚才实现的 `Difference` 类就能把这道题解决掉: + +```java +class Solution { + public int[] getModifiedArray(int length, int[][] updates) { + // nums 初始化为全 0 + int[] nums = new int[length]; + // 构造差分解法 + Difference df = new Difference(nums); + + for (int[] update : updates) { + int i = update[0]; + int j = update[1]; + int val = update[2]; + df.increment(i, j, val); + } + + return df.result(); + } +} +``` + +当然,实际的算法题可能需要我们对题目进行联想和抽象,不会这么直接地让你看出来要用差分数组技巧,这里看一下力扣第 1109 题「航班预订统计」: + + + +函数签名如下: + +```java +int[] corpFlightBookings(int[][] bookings, int n) +``` + +这个题目就在那绕弯弯,其实它就是个差分数组的题,我给你翻译一下: + +给你输入一个长度为 `n` 的数组 `nums`,其中所有元素都是 0。再给你输入一个 `bookings`,里面是若干三元组 `(i, j, k)`,每个三元组的含义就是要求你给 `nums` 数组的闭区间 `[i-1,j-1]` 中所有元素都加上 `k`。请你返回最后的 `nums` 数组是多少? + +> [!NOTE] +> 因为题目说的 `n` 是从 1 开始计数的,而数组索引从 0 开始,所以对于输入的三元组 `(i, j, k)`,数组区间应该对应 `[i-1,j-1]`。 + +这么一看,不就是一道标准的差分数组题嘛?我们可以直接复用刚才写的类: + +```java +class Solution { + public int[] corpFlightBookings(int[][] bookings, int n) { + // nums 初始化为全 0 + int[] nums = new int[n]; + // 构造差分解法 + Difference df = new Difference(nums); + + for (int[] booking : bookings) { + // 注意转成数组索引要减一哦 + int i = booking[0] - 1; + int j = booking[1] - 1; + int val = booking[2]; + // 对区间 nums[i..j] 增加 val + df.increment(i, j, val); + } + // 返回最终的结果数组 + return df.result(); + } +} +``` + +这道题就解决了。 + +还有一道很类似的题目是力扣第 1094 题「拼车」,我简单描述下题目: + +你是一个开公交车的司机,公交车的最大载客量为 `capacity`,沿途要经过若干车站,给你一份乘客行程表 `int[][] trips`,其中 `trips[i] = [num, start, end]` 代表着有 `num` 个旅客要从站点 `start` 上车,到站点 `end` 下车,请你计算是否能够一次把所有旅客运送完毕(不能超过最大载客量 `capacity`)。 + +函数签名如下: + +```java +boolean carPooling(int[][] trips, int capacity); +``` + +比如输入: + +``` +trips = [[2,1,5],[3,3,7]], capacity = 4 +``` + +这就不能一次运完,因为 `trips[1]` 最多只能上 2 人,否则车就会超载。 + +相信你已经能够联想到差分数组技巧了:**`trips[i]` 代表着一组区间操作,旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于 `capacity`,就说明可以不超载运输所有旅客**。 + +但问题是,差分数组的长度(车站的个数)应该是多少呢?题目没有直接给,但给出了数据取值范围: + +```java +0 <= trips[i][1] < trips[i][2] <= 1000 +``` + +车站编号从 0 开始,最多到 1000,也就是最多有 1001 个车站,那么我们的差分数组长度可以直接设置为 1001,这样索引刚好能够涵盖所有车站的编号: + +```java +class Solution { + public boolean carPooling(int[][] trips, int capacity) { + // 最多有 1001 个车站 + int[] nums = new int[1001]; + + // 构造差分解法 + Difference df = new Difference(nums); + + for (int[] trip : trips) { + // 乘客数量 + int val = trip[0]; + + // 第 trip[1] 站乘客上车 + int i = trip[1]; + + // 第 trip[2] 站乘客已经下车, + // 即乘客在车上的区间是 [trip[1], trip[2] - 1] + int j = trip[2] - 1; + + // 进行区间操作 + df.increment(i, j, val); + } + + int[] res = df.result(); + + // 客车自始至终都不应该超载 + for (int i = 0; i < res.length; i++) { + if (capacity < res[i]) { + return false; + } + } + return true; + } +} +``` + +至此,这道题也解决了。 + +差分数组和前缀和数组都是比较常见且巧妙的算法技巧,分别适用不同的场景,而且是会者不难,难者不会。所以,关于差分数组的使用,你学会了吗? + + + + + + + +
+
+引用本文的文章 + + - [二维数组的花式遍历技巧](https://labuladong.online/algo/practice-in-action/2d-array-traversal-summary/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [扫描线技巧:安排会议室](https://labuladong.online/algo/frequency-interview/scan-line-technique/) + - [算法刷题的重点和坑](https://labuladong.online/algo/intro/how-to-learn-algorithms/) + +

+ + + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" index f239d1b516..8d27023e17 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" @@ -1,90 +1,174 @@ # 常用的位操作 -本文分两部分,第一部分列举几个有趣的位操作,第二部分讲解算法中常用的 n & (n - 1) 操作,顺便把用到这个技巧的算法题列出来讲解一下。因为位操作很简单,所以假设读者已经了解与、或、异或这三种基本操作。 -位操作(Bit Manipulation)可以玩出很多奇技淫巧,但是这些技巧大部分都过于晦涩,没必要深究,读者只要记住一些有用的操作即可。 -### 一、几个有趣的位操作 +![](https://labuladong.online/algo/images/souyisou1.png) -1. 利用或操作 `|` 和空格将英文字符转换为小写 +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -```c -('a' | ' ') = 'a' -('A' | ' ') = 'a' -``` -2. 利用与操作 `&` 和下划线将英文字符转换为大写 -```c -('b' & '_') = 'B' -('B' & '_') = 'B' -``` +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: -3. 利用异或操作 `^` 和空格进行英文字符大小写互换 +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [136. Single Number](https://leetcode.com/problems/single-number/) | [136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | 🟢 | +| [191. Number of 1 Bits](https://leetcode.com/problems/number-of-1-bits/) | [191. 位1的个数](https://leetcode.cn/problems/number-of-1-bits/) | 🟢 | +| [231. Power of Two](https://leetcode.com/problems/power-of-two/) | [231. 2 的幂](https://leetcode.cn/problems/power-of-two/) | 🟢 | +| [268. Missing Number](https://leetcode.com/problems/missing-number/) | [268. 丢失的数字](https://leetcode.cn/problems/missing-number/) | 🟢 | -```c -('d' ^ ' ') = 'D' -('D' ^ ' ') = 'd' -``` +**-----------** -PS:以上操作能够产生奇特效果的原因在于 ASCII 编码。字符其实就是数字,恰巧这些字符对应的数字通过位运算就能得到正确的结果,有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。 -4. 判断两个数是否异号 -```c -int x = -1, y = 2; -bool f = ((x ^ y) < 0); // true +位操作(Bit Manipulation)可以有很多技巧,有一个叫做 Bit Twiddling Hacks 的网站收集了几乎所有位操作的黑科技玩法,网址如下: + +http://graphics.stanford.edu/~seander/bithacks.html + +但是这些技巧大部分都过于晦涩,我觉得可以作为字典查阅,没必要逐条深究。但我认为那些有趣的、有用的位运算技巧,是我们每个人需要掌握的。 + +所以本文由浅入深,先展示几个有趣(但没卵用)的位运算技巧,然后再汇总一些在算法题以及工程开发中常用的位运算技巧。 + +## 一、几个有趣的位操作 + -int x = 3, y = 2; -bool f = ((x ^ y) < 0); // false -``` -PS:这个技巧还是很实用的,利用的是补码编码的符号位。如果不用位运算来判断是否异号,需要使用 if else 分支,还挺麻烦的。读者可能想利用乘积或者商来判断两个数是否异号,但是这种处理方式可能造成溢出,从而出现错误。(关于补码编码和溢出,参见前文) -5. 交换两个数 -```c +```java +// 1. 利用或操作 `|` 和空格将英文字符转换为小写 +('a' | ' ') = 'a' +('A' | ' ') = 'a' + +// 2. 利用与操作 `&` 和下划线将英文字符转换为大写 +('b' & '_') = 'B' +('B' & '_') = 'B' + +// 3. 利用异或操作 `^` 和空格进行英文字符大小写互换 +('d' ^ ' ') = 'D' +('D' ^ ' ') = 'd' + +// 以上操作能够产生奇特效果的原因在于 ASCII 编码 +// ASCII 字符其实就是数字,恰巧空格和下划线对应的数字通过位运算就能改变大小写 +// 有兴趣的读者可以查 ASCII 码表自己算算,我就不展开讲了 + + +// 4. 不用临时变量交换两个数 int a = 1, b = 2; a ^= b; b ^= a; a ^= b; // 现在 a = 2, b = 1 -``` -6. 加一 -```c +// 5. 加一 int n = 1; n = -~n; // 现在 n = 2 -``` -7. 减一 -```c +// 6. 减一 int n = 2; n = ~-n; // 现在 n = 1 + + +// 7. 判断两个数是否异号 +int x = -1, y = 2; +boolean f = ((x ^ y) < 0); // true + +int x = 3, y = 2; +boolean f = ((x ^ y) < 0); // false ``` -PS:上面这三个操作就纯属装逼用的,没啥实际用处,大家了解了解乐呵一下就行。 -### 二、算法常用操作 n&(n-1) -这个操作是算法中常见的,作用是消除数字 n 的二进制表示中的最后一个 1。 +如果说前 6 个技巧的用处不大,这第 7 个技巧还是比较实用的,利用的是**补码编码**的符号位。整数编码最高位是符号位,负数的符号位是 1,非负数的符号位是 0,再借助异或的特性,可以判断出两个数字是否异号。 + +当然,如果不用位运算来判断是否异号,需要使用 if else 分支,还挺麻烦的。你可能想利用乘积来判断两个数是否异号,但是这种处理方式容易造成整型溢出,从而出现错误。 + + + + + + + +## `index & (arr.length - 1)` 的运用 + +我在 [单调栈解题套路](https://labuladong.online/algo/data-structure/monotonic-stack/) 中介绍过环形数组,其实就是利用求模(余数)的方式让数组看起来头尾相接形成一个环形,永远都走不完: + +```java +int[] arr = {1,2,3,4}; +int index = 0; +while (true) { + // 在环形数组中转圈 + print(arr[index % arr.length]); + index++; +} +// 输出:1,2,3,4,1,2,3,4,1,2,3,4... +``` + +但模运算 `%` 对计算机来说其实是一个比较昂贵的操作,所以我们可以用 `&` 运算来求余数: + +```java +int[] arr = {1,2,3,4}; +int index = 0; +while (true) { + // 在环形数组中转圈 + print(arr[index & (arr.length - 1)]); + index++; +} +// 输出:1,2,3,4,1,2,3,4,1,2,3,4... +``` + +> [!IMPORTANT] +> 注意这个技巧只适用于数组长度是 2 的幂次方的情况,比如 2、4、8、16、32 以此类推。至于如何将数组长度扩展为 2 的幂次方,这也是有比较巧妙的位运算算法的,可以参考 https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + +简单说,`& (arr.length - 1)` 这个位运算能够替代 `% arr.length` 的模运算,性能会更好一些。 + +那问题来了,现在是不断地 `index++`,你做到了循环遍历。但如果不断地 `index--`,还能做到环形数组的效果吗? + +答案是,如果你使用 `%` 求模的方式,那么当 `index` 小于 0 之后求模的结果也会出现负数,你需要特殊处理。但通过 `&` 与运算的方式,`index` 不会出现负数,依然可以正常工作: + +```java +int[] arr = {1,2,3,4}; +int index = 0; +while (true) { + // 在环形数组中转圈 + print(arr[index & (arr.length - 1)]); + index--; +} +// 输出:1,4,3,2,1,4,3,2,1,4,3,2,1... +``` + +我们自己写代码一般用不到这个技巧,但在学习一些其他代码库时可能会经常看到,这里留个印象,到时候就不会懵逼了。 + +## `n & (n-1)` 的运用 + +**`n & (n-1)` 这个操作在算法中比较常见,作用是消除数字 `n` 的二进制表示中的最后一个 1**。 看个图就很容易理解了: -![n](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/1.png) +![](https://labuladong.online/algo/images/bit-op/1.png) + +其核心逻辑就是,`n - 1` 一定可以消除最后一个 1,同时把其后的 0 都变成 1,这样再和 `n` 做一次 `&` 运算,就可以仅仅把最后一个 1 变成 0 了。 + +### 计算汉明权重(Hamming Weight) + +这是力扣第 191 题「位 1 的个数」: + + + -1. 计算汉明权重(Hamming Weight) -![title](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/title.png) + -就是让你返回 n 的二进制表示中有几个 1。因为 n & (n - 1) 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 n 变成 0 为止。 -```cpp -int hammingWeight(uint32_t n) { + +就是让你返回 `n` 的二进制表示中有几个 1。因为 `n & (n - 1)` 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 `n` 变成 0 为止。 + +```java +int hammingWeight(int n) { int res = 0; while (n != 0) { n = n & (n - 1); @@ -94,34 +178,169 @@ int hammingWeight(uint32_t n) { } ``` -1. 判断一个数是不是 2 的指数 +### 判断 2 的指数 + +力扣第 231 题「2 的幂」就是这个问题。 一个数如果是 2 的指数,那么它的二进制表示一定只含有一个 1: -```cpp +```java 2^0 = 1 = 0b0001 2^1 = 2 = 0b0010 2^2 = 4 = 0b0100 ``` -如果使用位运算技巧就很简单了(注意运算符优先级,括号不可以省略): -```cpp -bool isPowerOfTwo(int n) { + +如果使用 `n & (n-1)` 的技巧就很简单了(注意运算符优先级,括号不可以省略): + +```java +boolean isPowerOfTwo(int n) { if (n <= 0) return false; return (n & (n - 1)) == 0; } ``` -以上便是一些有趣/常用的位操作。其实位操作的技巧很多,有一个叫做 Bit Twiddling Hacks 的外国网站收集了几乎所有位操作的黑科技玩法,感兴趣的读者可以点击「阅读原文」按钮查看。 +## `a ^ a = 0` 的运用 + +异或运算的性质是需要我们牢记的: + +一个数和它本身做异或运算结果为 0,即 `a ^ a = 0`;一个数和 0 做异或运算的结果为它本身,即 `a ^ 0 = a`。 + +### 查找只出现一次的元素 + +这是力扣第 136 题「只出现一次的数字」: + + + +对于这道题目,我们只要把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素: + +```java +int singleNumber(int[] nums) { + int res = 0; + for (int n : nums) { + res ^= n; + } + return res; +} +``` + +### 寻找缺失的元素 + +这是力扣第 268 题「丢失的数字」: + + + +给一个长度为 `n` 的数组,其索引应该在 `[0,n)`,但是现在你要装进去 `n + 1` 个元素 `[0,n]`,那么肯定有一个元素装不下嘛,请你找出这个缺失的元素。 + +这道题不难的,我们应该很容易想到,把这个数组排个序,然后遍历一遍,不就很容易找到缺失的那个元素了吗? + +或者说,借助数据结构的特性,用一个 HashSet 把数组里出现的数字都储存下来,再遍历 `[0,n]` 之间的数字,去 HashSet 中查询,也可以很容易查出那个缺失的元素。 + +排序解法的时间复杂度是 O(NlogN),HashSet 的解法时间复杂度是 O(N),但是还需要 O(N) 的空间复杂度存储 HashSet。 + +这个问题其实还有一个特别简单的解法:等差数列求和公式。 + +题目的意思可以这样理解:现在有个等差数列 `0, 1, 2,..., n`,其中少了某一个数字,请你把它找出来。那这个数字不就是 `sum(0,1,..n) - sum(nums)` 嘛? + +```java +int missingNumber(int[] nums) { + int n = nums.length; + // 虽然题目给的数据范围不大,但严谨起见,用 long 类型防止整型溢出 + // 求和公式:(首项 + 末项) * 项数 / 2 + long expect = (0 + n) * (n + 1) / 2; + long sum = 0; + for (int x : nums) { + sum += x; + } + return (int)(expect - sum); +} +``` + +不过,本文的主题是位运算,我们来讲讲如何利用位运算技巧来解决这道题。 + +再回顾一下异或运算的性质:一个数和它本身做异或运算结果为 0,一个数和 0 做异或运算还是它本身。 + +而且异或运算满足交换律和结合律,也就是说: + +```java +2 ^ 3 ^ 2 = 3 ^ (2 ^ 2) = 3 ^ 0 = 3 +``` + +而这道题索就可以通过这些性质巧妙算出缺失的那个元素,比如说 `nums = [0,3,1,4]`: + +![](https://labuladong.online/algo/images/missing-elem/1.jpg) + +为了容易理解,我们假设先把索引补一位,然后让每个元素和自己相等的索引相对应: + +![](https://labuladong.online/algo/images/missing-elem/2.jpg) + +这样做了之后,就可以发现除了缺失元素之外,所有的索引和元素都组成一对儿了,现在如果把这个落单的索引 2 找出来,也就找到了缺失的那个元素。 + +如何找这个落单的数字呢,**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**,也就达到了我们的目的: + +```java +class Solution { + public int missingNumber(int[] nums) { + int n = nums.length; + int res = 0; + // 先和新补的索引异或一下 + res ^= n; + // 和其他的元素、索引做异或 + for (int i = 0; i < n; i++) + res ^= i ^ nums[i]; + return res; + } +} +``` + +![](https://labuladong.online/algo/images/missing-elem/3.jpg) + + + +由于异或运算满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素。 + +到这里,常见的位运算差不多都讲完了。这些技巧就是会者不难难者不会,也不需要死记硬背,只要有个印象就完全够用了。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】哈希表更多习题](https://labuladong.online/algo/problem-set/hash-table/) + - [【强化练习】用「遍历」思维解题 I](https://labuladong.online/algo/problem-set/binary-tree-traverse-i/) + - [一文秒杀所有丑数系列问题](https://labuladong.online/algo/frequency-interview/ugly-number-summary/) + - [如何同时寻找缺失和重复的元素](https://labuladong.online/algo/frequency-interview/mismatch-set/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1457. Pseudo-Palindromic Paths in a Binary Tree](https://leetcode.com/problems/pseudo-palindromic-paths-in-a-binary-tree/?show=1) | [1457. 二叉树中的伪回文路径](https://leetcode.cn/problems/pseudo-palindromic-paths-in-a-binary-tree/?show=1) | 🟠 | +| [389. Find the Difference](https://leetcode.com/problems/find-the-difference/?show=1) | [389. 找不同](https://leetcode.cn/problems/find-the-difference/?show=1) | 🟢 | +| - | [剑指 Offer 15. 二进制中1的个数](https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/?show=1) | 🟢 | + +
+
-坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:twoSum问题的核心思想](../算法思维系列/twoSum问题的核心思想.md) -[下一篇:拆解复杂问题:实现计算器](../数据结构系列/实现计算器.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" index 5207da39ad..0cbf7e7f24 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" @@ -1,9 +1,32 @@ # 洗牌算法 +

+GitHub + + + +

+ +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [384. Shuffle an Array](https://leetcode.com/problems/shuffle-an-array/) | [384. 打乱数组](https://leetcode.cn/problems/shuffle-an-array/) | 🟠 + +**-----------** + 我知道大家会各种花式排序算法,但是如果叫你打乱一个数组,你是否能做到胸有成竹?即便你拍脑袋想出一个算法,怎么证明你的算法就是正确的呢?乱序算法不像排序算法,结果唯一可以很容易检验,因为「乱」可以有很多种,你怎么能证明你的算法是「真的乱」呢? 所以我们面临两个问题: + 1. 什么叫做「真的乱」? + 2. 设计怎样的算法来打乱数组才能做到「真的乱」? 这种算法称为「随机乱置算法」或者「洗牌算法」。 @@ -14,6 +37,7 @@ 此类算法都是靠随机选取元素交换来获取随机性,直接看代码(伪码),该算法有 4 种形式,都是正确的: + ```java // 得到一个在闭区间 [min, max] 内的随机整数 int randInt(int min, int max); @@ -44,10 +68,11 @@ void shuffle(int[] arr) { ``` -**分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能,否则就是错误的。**这个很好解释,因为一个长度为 n 的数组的全排列就有 n! 种,也就是说打乱结果总共有 n! 种。算法必须能够反映这个事实,才是正确的。 +**分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能,否则就是错误的**。这个很好解释,因为一个长度为 n 的数组的全排列就有 n! 种,也就是说打乱结果总共有 n! 种。算法必须能够反映这个事实,才是正确的。 我们先用这个准则分析一下**第一种写法**的正确性: + ```java // 假设传入这样一个 arr int[] arr = {1,3,5,7,9}; @@ -63,15 +88,15 @@ void shuffle(int[] arr) { for 循环第一轮迭代时,`i = 0`,`rand` 的取值范围是 `[0, 4]`,有 5 个可能的取值。 -![第一次](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/1.png) +![](https://labuladong.online/algo/images/洗牌算法/1.png) for 循环第二轮迭代时,`i = 1`,`rand` 的取值范围是 `[1, 4]`,有 4 个可能的取值。 -![第二次](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/2.png) +![](https://labuladong.online/algo/images/洗牌算法/2.png) 后面以此类推,直到最后一次迭代,`i = 4`,`rand` 的取值范围是 `[4, 4]`,只有 1 个可能的取值。 -![最后一次](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/3.png) +![](https://labuladong.online/algo/images/洗牌算法/3.png) 可以看到,整个过程产生的所有可能结果有 `n! = 5! = 5*4*3*2*1` 种,所以这个算法是正确的。 @@ -90,6 +115,7 @@ for 循环第二轮迭代时,`i = 1`,`rand` 的取值范围是 `[1, 4]`, 如果读者思考过洗牌算法,可能会想出如下的算法,但是**这种写法是错误的**: + ```java void shuffle(int[] arr) { int n = arr.length(); @@ -102,21 +128,21 @@ void shuffle(int[] arr) { } ``` -现在你应该明白这种写法为什么会错误了。因为这种写法得到的所有可能结果有 $n^n$ 种,而不是 $n!$ 种,而且 $n^n$ 不可能是 $n!$ 的整数倍。 +现在你应该明白这种写法为什么会错误了。因为这种写法得到的所有可能结果有 `n^n` 种,而不是 `n!` 种,而且 `n^n` 不可能是 `n!` 的整数倍。 -比如说 `arr = {1,2,3}`,正确的结果应该有 $3!= 6$ 种可能,而这种写法总共有 $3^3 = 27$ 种可能结果。因为 27 不能被 6 整除,所以一定有某些情况被「偏袒」了,也就是说某些情况出现的概率会大一些,所以这种打乱结果不算「真的乱」。 +比如说 `arr = {1,2,3}`,正确的结果应该有 `3!= 6` 种可能,而这种写法总共有 `3^3 = 27` 种可能结果。因为 27 不能被 6 整除,所以一定有某些情况被「偏袒」了,也就是说某些情况出现的概率会大一些,所以这种打乱结果不算「真的乱」。 上面我们从直觉上简单解释了洗牌算法正确的准则,没有数学证明,我想大家也懒得证明。对于概率问题我们可以使用「蒙特卡罗方法」进行简单验证。 ### 二、蒙特卡罗方法验证正确性 -洗牌算法,或者说随机乱置算法的**正确性衡量标准是:对于每种可能的结果出现的概率必须相等,也就是说要足够随机。** +洗牌算法,或者说随机乱置算法的**正确性衡量标准是:对于每种可能的结果出现的概率必须相等,也就是说要足够随机**。 如果不用数学严格证明概率相等,可以用蒙特卡罗方法近似地估计出概率是否相等,结果是否足够随机。 记得高中有道数学题:往一个正方形里面随机打点,这个正方形里紧贴着一个圆,告诉你打点的总数和落在圆里的点的数量,让你计算圆周率。 -![正方形](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/4.png) +![](https://labuladong.online/algo/images/洗牌算法/4.png) 这其实就是利用了蒙特卡罗方法:当打的点足够多的时候,点的数量就可以近似代表图形的面积。通过面积公式,由正方形和圆的面积比值是可以很容易推出圆周率的。当然打的点越多,算出的圆周率越准确,充分体现了大力出奇迹的真理。 @@ -124,10 +150,11 @@ void shuffle(int[] arr) { **第一种思路**,我们把数组 arr 的所有排列组合都列举出来,做成一个直方图(假设 arr = {1,2,3}): -![直方图](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/5.jpg) +![](https://labuladong.online/algo/images/洗牌算法/5.jpg) 每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。写一下这个思路的伪代码: + ```java void shuffle(int[] arr); @@ -169,7 +196,7 @@ for (int feq : count) print(feq / N + " "); // 频率 ``` -![直方图](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/6.png) +![](https://labuladong.online/algo/images/洗牌算法/6.png) 这种思路也是可行的,而且避免了阶乘级的空间复杂度,但是多了嵌套 for 循环,时间复杂度高一点。不过由于我们的测试数据量不会有多大,这些问题都可以忽略。 @@ -181,13 +208,108 @@ for (int feq : count) 第二部分写了洗牌算法正确性的衡量标准,即每种随机结果出现的概率必须相等。如果我们不用严格的数学证明,可以通过蒙特卡罗方法大力出奇迹,粗略验证算法的正确性。蒙特卡罗方法也有不同的思路,不过要求不必太严格,因为我们只是寻求一个简单的验证。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:几个反直觉的概率问题](../算法思维系列/几个反直觉的概率问题.md) -[下一篇:递归详解](../算法思维系列/递归详解.md) +**_____________** + +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: + +![](https://labuladong.online/algo/images/souyisou2.png) + +======其他语言代码====== + +[384.打乱数组](https://leetcode-cn.com/problems/shuffle-an-array) + +### javascript + +```js +// 得到一个在闭区间 [min, max] 内的随机整数 +const randInt = function (minNum, maxNum) { + return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10); +}; + + +// 第一种写法 +let shuffle = function (arr) { + const swap = (i, j) => { + let t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; + } + + let n = arr.length; + + /******** 区别只有这两行 ********/ + for (let i = 0; i < n; i++) { + // 从 i 到最后随机选一个元素 + let rand = randInt(i, n - 1); + /*************************/ + // 交换 i rand 上的元素 + swap(i, rand); + } +} + +// 第二种写法 +let shuffle = function (arr) { + const swap = (i, j) => { + let t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; + } + + let n = arr.length; + + + /******** 区别只有这两行 ********/ + for (let i = 0; i < n - 1; i++) { + let rand = randInt(i, n - 1); + /*************************/ + // 交换 i rand 上的元素 + swap(i, rand); + } +} + +// 第三种写法 +let shuffle = function (arr) { + const swap = (i, j) => { + let t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; + } + + let n = arr.length; + + + /******** 区别只有这两行 ********/ + for (let i = n - 1; i >= 0; i--) { + let rand = randInt(0, i); + + /*************************/ + // 交换 i rand 上的元素 + swap(i, rand); + } +} + +// 第四种写法 +let shuffle = function (arr) { + const swap = (i, j) => { + let t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; + } + + let n = arr.length; + + + /******** 区别只有这两行 ********/ + for (let i = n - 1; i > 0; i--) { + let rand = randInt(0, i); + /*************************/ + // 交换 i rand 上的元素 + swap(i, rand); + } +} +``` -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" deleted file mode 100644 index 57a5d7a93c..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" +++ /dev/null @@ -1,302 +0,0 @@ -# 滑动窗口技巧 - -本文详解「滑动窗口」这种高级双指针技巧的算法框架,带你秒杀几道高难度的子字符串匹配问题。 - -LeetCode 上至少有 9 道题目可以用此方法高效解决。但是有几道是 VIP 题目,有几道题目虽不难但太复杂,所以本文只选择点赞最高,较为经典的,最能够讲明白的三道题来讲解。第一题为了让读者掌握算法模板,篇幅相对长,后两题就基本秒杀了。 - -本文代码为 C++ 实现,不会用到什么编程方面的奇技淫巧,但是还是简单介绍一下一些用到的数据结构,以免有的读者因为语言的细节问题阻碍对算法思想的理解: - -`unordered_map` 就是哈希表(字典),它的一个方法 count(key) 相当于 containsKey(key) 可以判断键 key 是否存在。 - -可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。 - -所以代码中多次出现的 `map[key]++` 相当于 Java 的 `map.put(key, map.getOrDefault(key, 0) + 1)`。 - -本文大部分代码都是图片形式,可以点开放大,更重要的是可以左右滑动方便对比代码。下面进入正题。 - -### 一、最小覆盖子串 - -![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title1.png) - -题目不难理解,就是说要在 S(source) 中找到包含 T(target) 中全部字母的一个子串,顺序无所谓,但这个子串一定是所有可能子串中最短的。 - -如果我们使用暴力解法,代码大概是这样的: - -```java -for (int i = 0; i < s.size(); i++) - for (int j = i + 1; j < s.size(); j++) - if s[i:j] 包含 t 的所有字母: - 更新答案 -``` - -思路很直接吧,但是显然,这个算法的复杂度肯定大于 O(N^2) 了,不好。 - -滑动窗口算法的思路是这样: - -1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。 - -2、我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。 - -3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。 - -4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。 - -这个思路其实也不难,**第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。**左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。 - -下面画图理解一下,needs 和 window 相当于计数器,分别记录 T 中字符出现次数和窗口中的相应字符的出现次数。 - -初始状态: - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/0.png) - -增加 right,直到窗口 [left, right] 包含了 T 中所有字符: - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/1.png) - - -现在开始增加 left,缩小窗口 [left, right]。 - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/2.png) - -直到窗口中的字符串不再符合要求,left 不再继续移动。 - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/3.png) - - -之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。 - -如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。至于如何具体到问题,如何得出此题的答案,都是编程问题,等会提供一套模板,理解一下就会了。 - -上述过程可以简单地写出如下伪码框架: - -```cpp -string s, t; -// 在 s 中寻找 t 的「最小覆盖子串」 -int left = 0, right = 0; -string res = s; - -while(right < s.size()) { - window.add(s[right]); - right++; - // 如果符合要求,移动 left 缩小窗口 - while (window 符合要求) { - // 如果这个窗口的子串更短,则更新 res - res = minLen(res, window); - window.remove(s[left]); - left++; - } -} -return res; -``` - -如果上述代码你也能够理解,那么你离解题更近了一步。现在就剩下一个比较棘手的问题:如何判断 window 即子串 s[left...right] 是否符合要求,是否包含 t 的所有字符呢? - -可以用两个哈希表当作计数器解决。用一个哈希表 needs 记录字符串 t 中包含的字符及出现次数,用另一个哈希表 window 记录当前「窗口」中包含的字符及出现的次数,如果 window 包含所有 needs 中的键,且这些键对应的值都大于等于 needs 中的值,那么就可以知道当前「窗口」符合要求了,可以开始移动 left 指针了。 - -现在将上面的框架继续细化: - -```cpp -string s, t; -// 在 s 中寻找 t 的「最小覆盖子串」 -int left = 0, right = 0; -string res = s; - -// 相当于两个计数器 -unordered_map window; -unordered_map needs; -for (char c : t) needs[c]++; - -// 记录 window 中已经有多少字符符合要求了 -int match = 0; - -while (right < s.size()) { - char c1 = s[right]; - if (needs.count(c1)) { - window[c1]++; // 加入 window - if (window[c1] == needs[c1]) - // 字符 c1 的出现次数符合要求了 - match++; - } - right++; - - // window 中的字符串已符合 needs 的要求了 - while (match == needs.size()) { - // 更新结果 res - res = minLen(res, window); - char c2 = s[left]; - if (needs.count(c2)) { - window[c2]--; // 移出 window - if (window[c2] < needs[c2]) - // 字符 c2 出现次数不再符合要求 - match--; - } - left++; - } -} -return res; -``` - -上述代码已经具备完整的逻辑了,只有一处伪码,即更新 res 的地方,不过这个问题太好解决了,直接看解法吧! - -```cpp -string minWindow(string s, string t) { - // 记录最短子串的开始位置和长度 - int start = 0, minLen = INT_MAX; - int left = 0, right = 0; - - unordered_map window; - unordered_map needs; - for (char c : t) needs[c]++; - - int match = 0; - - while (right < s.size()) { - char c1 = s[right]; - if (needs.count(c1)) { - window[c1]++; - if (window[c1] == needs[c1]) - match++; - } - right++; - - while (match == needs.size()) { - if (right - left < minLen) { - // 更新最小子串的位置和长度 - start = left; - minLen = right - left; - } - char c2 = s[left]; - if (needs.count(c2)) { - window[c2]--; - if (window[c2] < needs[c2]) - match--; - } - left++; - } - } - return minLen == INT_MAX ? - "" : s.substr(start, minLen); -} -``` - -如果直接甩给你这么一大段代码,我想你的心态是爆炸的,但是通过之前的步步跟进,你是否能够理解这个算法的内在逻辑呢?你是否能清晰看出该算法的结构呢? - -这个算法的时间复杂度是 O(M + N),M 和 N 分别是字符串 S 和 T 的长度。因为我们先用 for 循环遍历了字符串 T 来初始化 needs,时间 O(N),之后的两个 while 循环最多执行 2M 次,时间 O(M)。 - -读者也许认为嵌套的 while 循环复杂度应该是平方级,但是你这样想,while 执行的次数就是双指针 left 和 right 走的总路程,最多是 2M 嘛。 - - -### 二、找到字符串中所有字母异位词 - -![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title2.png) - -这道题的难度是 Easy,但是评论区点赞最多的一条是这样: - -`How can this problem be marked as easy???` - -实际上,这个 Easy 是属于了解双指针技巧的人的,只要把上一道题的代码改中更新 res 部分的代码稍加修改就成了这道题的解: - -```cpp -vector findAnagrams(string s, string t) { - // 用数组记录答案 - vector res; - int left = 0, right = 0; - unordered_map needs; - unordered_map window; - for (char c : t) needs[c]++; - int match = 0; - - while (right < s.size()) { - char c1 = s[right]; - if (needs.count(c1)) { - window[c1]++; - if (window[c1] == needs[c1]) - match++; - } - right++; - - while (match == needs.size()) { - // 如果 window 的大小合适 - // 就把起始索引 left 加入结果 - if (right - left == t.size()) { - res.push_back(left); - } - char c2 = s[left]; - if (needs.count(c2)) { - window[c2]--; - if (window[c2] < needs[c2]) - match--; - } - left++; - } - } - return res; -} -``` - -因为这道题和上一道的场景类似,也需要 window 中包含串 t 的所有字符,但上一道题要找长度最短的子串,这道题要找长度相同的子串,也就是「字母异位词」嘛。 - -### 三、无重复字符的最长子串 - -![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title3.png) - -遇到子串问题,首先想到的就是滑动窗口技巧。 - -类似之前的思路,使用 window 作为计数器记录窗口中的字符出现次数,然后先向右移动 right,当 window 中出现重复字符时,开始移动 left 缩小窗口,如此往复: - -```cpp -int lengthOfLongestSubstring(string s) { - int left = 0, right = 0; - unordered_map window; - int res = 0; // 记录最长长度 - - while (right < s.size()) { - char c1 = s[right]; - window[c1]++; - right++; - // 如果 window 中出现重复字符 - // 开始移动 left 缩小窗口 - while (window[c1] > 1) { - char c2 = s[left]; - window[c2]--; - left++; - } - res = max(res, right - left); - } - return res; -} -``` - -需要注意的是,因为我们要求的是最长子串,所以需要在每次移动 right 增大窗口时更新 res,而不是像之前的题目在移动 left 缩小窗口时更新 res。 - -### 最后总结 - -通过上面三道题,我们可以总结出滑动窗口算法的抽象思想: - -```java -int left = 0, right = 0; - -while (right < s.size()) { - window.add(s[right]); - right++; - - while (valid) { - window.remove(s[left]); - left++; - } -} -``` - -其中 window 的数据类型可以视具体情况而定,比如上述题目都使用哈希表充当计数器,当然你也可以用一个数组实现同样效果,因为我们只处理英文字母。 - -稍微麻烦的地方就是这个 valid 条件,为了实现这个条件的实时更新,我们可能会写很多代码。比如前两道题,看起来解法篇幅那么长,实际上思想还是很简单,只是大多数代码都在处理这个问题而已。 - -如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~ - -![公众号 labuladong](../pictures/labuladong.png) - -[上一篇:二分查找解题框架](../算法思维系列/二分查找详解.md) - -[下一篇:双指针技巧解题框架](../算法思维系列/双指针技巧.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" new file mode 100644 index 0000000000..f48ecdcd71 --- /dev/null +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" @@ -0,0 +1,631 @@ +# 滑动窗口算法核心代码模板 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [3. Longest Substring Without Repeating Characters](https://leetcode.com/problems/longest-substring-without-repeating-characters/) | [3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | 🟠 | +| [438. Find All Anagrams in a String](https://leetcode.com/problems/find-all-anagrams-in-a-string/) | [438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) | 🟠 | +| [567. Permutation in String](https://leetcode.com/problems/permutation-in-string/) | [567. 字符串的排列](https://leetcode.cn/problems/permutation-in-string/) | 🟠 | +| [76. Minimum Window Substring](https://leetcode.com/problems/minimum-window-substring/) | [76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) | 🔴 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) + +关于双指针的快慢指针和左右指针的用法,可以参见前文 [双指针技巧汇总](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/),本文就解决一类最难掌握的双指针技巧:滑动窗口技巧。并总结出一套框架,可以保你闭着眼睛都能写出正确的解法。 + +## 滑动窗口框架概览 + +**滑动窗口算法技巧主要用来解决子数组问题,比如让你寻找符合某个条件的最长/最短子数组**。 + +如果用暴力解的话,你需要嵌套 for 循环这样穷举所有子数组,时间复杂度是 $O(N^2)$: + + + + + +```java +for (int i = 0; i < nums.length; i++) { + for (int j = i; j < nums.length; j++) { + // nums[i, j] 是一个子数组 + } +} +``` + + + +滑动窗口算法技巧的思路也不难,就是维护一个窗口,不断滑动,然后更新答案,该算法的大致逻辑如下: + + + + + +```java +int left = 0, right = 0; + +while (right < nums.size()) { + // 增大窗口 + window.addLast(nums[right]); + right++; + + while (window needs shrink) { + // 缩小窗口 + window.removeFirst(nums[left]); + left++; + } +} +``` + + + +基于滑动窗口算法框架写出的代码,时间复杂度是 $O(N)$,比嵌套 for 循环的暴力解法效率高。 + +::: info 为啥是 $O(N)$? + +肯定有读者要问了,你这个滑动窗口框架不也用了一个嵌套 while 循环?为啥复杂度是 $O(N)$ 呢? + +简单说,指针 `left, right` 不会回退(它们的值只增不减),所以字符串/数组中的每个元素都只会进入窗口一次,然后被移出窗口一次,不会说有某些元素多次进入和离开窗口,所以算法的时间复杂度就和字符串/数组的长度成正比。 + +反观嵌套 for 循环的暴力解法,那个 `j` 会回退,所以某些元素会进入和离开窗口多次,所以时间复杂度就是 $O(N^2)$ 了。 + +我在 [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) 有具体教大家如何从理论上估算时间空间复杂度,这里就不展开了。 + +::: + +::: info 为啥滑动窗口能在 $O(N)$ 的时间穷举子数组? + +这个问题本身就是错误的,**滑动窗口并不能穷举出所有子串**。要想穷举出所有子串,必须用那个嵌套 for 循环。 + +然而对于某些题目,并不需要穷举所有子串,就能找到题目想要的答案。滑动窗口就是这种场景下的一套算法模板,帮你对穷举过程进行剪枝优化,避免冗余计算。 + +所以在 [算法的本质](https://labuladong.online/algo/essential-technique/algorithm-summary/) 中我把滑动窗口算法归为「如何聪明地穷举」一类。 + +::: + +其实困扰大家的,不是算法的思路,而是各种细节问题。比如说如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。即便你明白了这些细节,代码也容易出 bug,找 bug 还不知道怎么找,真的挺让人心烦的。 + +**所以今天我就写一套滑动窗口算法的代码框架,我连再哪里做输出 debug 都给你写好了,以后遇到相关的问题,你就默写出来如下框架然后改三个地方就行,保证不会出 bug**。 + +因为本文的例题大多是子串相关的题目,字符串实际上就是数组,所以我就把输入设置成字符串了。你做题的时候根据具体题目自行变通即可: + +```java +// 滑动窗口算法伪码框架 +void slidingWindow(String s) { + // 用合适的数据结构记录窗口中的数据,根据具体场景变通 + // 比如说,我想记录窗口中元素出现的次数,就用 map + // 如果我想记录窗口中的元素和,就可以只用一个 int + Object window = ... + + int left = 0, right = 0; + while (right < s.length()) { + // c 是将移入窗口的字符 + char c = s[right]; + window.add(c) + // 增大窗口 + right++; + // 进行窗口内数据的一系列更新 + ... + + // *** debug 输出的位置 *** + // 注意在最终的解法代码中不要 print + // 因为 IO 操作很耗时,可能导致超时 + printf("window: [%d, %d)\n", left, right); + // *********************** + + // 判断左侧窗口是否要收缩 + while (left < right && window needs shrink) { + // d 是将移出窗口的字符 + char d = s[left]; + window.remove(d) + // 缩小窗口 + left++; + // 进行窗口内数据的一系列更新 + ... + } + } +} +``` + +**框架中两处 `...` 表示的更新窗口数据的地方,在具体的题目中,你需要做的就是往这里面填代码逻辑**。而且,这两个 `...` 处的操作分别是扩大和缩小窗口的更新操作,等会你会发现它们操作是完全对称的。 + +说句题外话,有些读者评论我这个框架,说散列表速度慢,不如用数组代替散列表;还有些人喜欢把代码写得特别短小,说我这样代码太多余,速度不够快。我的意见是,算法主要看时间复杂度,你能确保自己的时间复杂度最优就行了。至于 LeetCode 的运行速度,那个有点玄学,只要不是慢的离谱就没啥问题,根本不值得你从编译层面优化,不要舍本逐末…… + +再说,我的算法教程重点在于算法思想,你先做到能把框架思维运用自如,然后随便你魔改代码好吧,保你怎么写都能写对。 + +言归正传,下面就直接上四道力扣原题来套这个框架,其中第一道题会详细说明其原理,后面四道就直接闭眼睛秒杀了。 + + + + + + + +## 一、最小覆盖子串 + +先来看看力扣第 76 题「最小覆盖子串」难度 Hard: + + + +就是说要在 `S`(source) 中找到包含 `T`(target) 中全部字母的一个子串,且这个子串一定是所有可能子串中最短的。 + +如果我们使用暴力解法,代码大概是这样的: + + + + + +```java +for (int i = 0; i < s.length(); i++) + for (int j = i + 1; j < s.length(); j++) + if s[i:j] 包含 t 的所有字母: + 更新答案 +``` + + + +思路很直接,但是显然,这个算法的复杂度肯定大于 O(N^2) 了,不好。 + +**滑动窗口算法的思路是这样**: + +1、我们在字符串 `S` 中使用双指针中的左右指针技巧,初始化 `left = right = 0`,把索引**左闭右开**区间 `[left, right)` 称为一个「窗口」。 + +::: tip 为什么要「左闭右开」区间 + +**理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的**。 + +因为这样初始化 `left = right = 0` 时区间 `[0, 0)` 中没有元素,但只要让 `right` 向右移动(扩大)一位,区间 `[0, 1)` 就包含一个元素 `0` 了。 + +如果你设置为两端都开的区间,那么让 `right` 向右移动一位后开区间 `(0, 1)` 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 `[0, 0]` 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。 + +::: + +2、我们先不断地增加 `right` 指针扩大窗口 `[left, right)`,直到窗口中的字符串符合要求(包含了 `T` 中的所有字符)。 + +3、此时,我们停止增加 `right`,转而不断增加 `left` 指针缩小窗口 `[left, right)`,直到窗口中的字符串不再符合要求(不包含 `T` 中的所有字符了)。同时,每次增加 `left`,我们都要更新一轮结果。 + +4、重复第 2 和第 3 步,直到 `right` 到达字符串 `S` 的尽头。 + +这个思路其实也不难,**第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解**,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,就好像一条毛毛虫,一伸一缩,不断向右滑动,这就是「滑动窗口」这个名字的来历。 + + + + + + + +下面画图理解一下,`needs` 和 `window` 相当于计数器,分别记录 `T` 中字符出现次数和「窗口」中的相应字符的出现次数。 + +初始状态: + +![](https://labuladong.online/algo/images/slidingwindow/1.png) + +增加 `right`,直到窗口 `[left, right)` 包含了 `T` 中所有字符: + +![](https://labuladong.online/algo/images/slidingwindow/2.png) + +现在开始增加 `left`,缩小窗口 `[left, right)`: + +![](https://labuladong.online/algo/images/slidingwindow/3.png) + +直到窗口中的字符串不再符合要求,`left` 不再继续移动: + +![](https://labuladong.online/algo/images/slidingwindow/4.png) + +之后重复上述过程,先移动 `right`,再移动 `left`…… 直到 `right` 指针到达字符串 `S` 的末端,算法结束。 + +如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。**现在我们来看看这个滑动窗口代码框架怎么用**: + +首先,初始化 `window` 和 `need` 两个哈希表,记录窗口中的字符和需要凑齐的字符: + + + + + +```java +// 记录 window 中的字符出现次数 +HashMap window = new HashMap<>(); +// 记录所需的字符出现次数 +HashMap need = new HashMap<>(); +for (int i = 0; i < t.length(); i++) { + char c = t.charAt(i); + need.put(c, need.getOrDefault(c, 0) + 1); +} +``` + + + +然后,使用 `left` 和 `right` 变量初始化窗口的两端,不要忘了,区间 `[left, right)` 是左闭右开的,所以初始情况下窗口没有包含任何元素: + + + + + +```java +int left = 0, right = 0; +int valid = 0; +while (right < s.length()) { + // c 是将移入窗口的字符 + char c = s.charAt(right); + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + ... +} +``` + + + +**其中 `valid` 变量表示窗口中满足 `need` 条件的字符个数**,如果 `valid` 和 `need.size` 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 `T`。 + +**现在开始套模板,只需要思考以下几个问题**: + +1、什么时候应该移动 `right` 扩大窗口?窗口加入字符时,应该更新哪些数据? + +2、什么时候窗口应该暂停扩大,开始移动 `left` 缩小窗口?从窗口移出字符时,应该更新哪些数据? + +3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新? + +如果一个字符进入窗口,应该增加 `window` 计数器;如果一个字符将移出窗口的时候,应该减少 `window` 计数器;当 `valid` 满足 `need` 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。 + +下面是完整代码: + +```java +class Solution { + public String minWindow(String s, String t) { + Map need = new HashMap<>(); + Map window = new HashMap<>(); + for (char c : t.toCharArray()) { + need.put(c, need.getOrDefault(c, 0) + 1); + } + + int left = 0, right = 0; + int valid = 0; + // 记录最小覆盖子串的起始索引及长度 + int start = 0, len = Integer.MAX_VALUE; + while (right < s.length()) { + // c 是将移入窗口的字符 + char c = s.charAt(right); + // 扩大窗口 + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (valid == need.size()) { + // 在这里更新最小覆盖子串 + if (right - left < len) { + start = left; + len = right - left; + } + // d 是将移出窗口的字符 + char d = s.charAt(left); + // 缩小窗口 + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) + valid--; + window.put(d, window.get(d) - 1); + } + } + } + // 返回最小覆盖子串 + return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len); + } +} +``` + + + +你可以点开下面的可视化面板,多次点击 while (right < s.length) 这一行代码,即可看到滑动窗口 `[left, right)` 的滑动过程: + + + +::: warning 使用 Java 的读者请注意 + +对 Java 包装类进行比较时要尤为小心,`Integer`,`String` 等类型应该用 `equals` 方法判定相等,而不能直接用等号 `==`,否则会出错。所以在缩小窗口更新数据的时候,不能直接写为 `window.get(d) == need.get(d)`,而要用 `window.get(d).equals(need.get(d))`,之后的题目代码同理。 + +::: + +上面的代码中,当我们发现某个字符在 `window` 的数量满足了 `need` 的需要,就要更新 `valid`,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。 + +当 `valid == need.size()` 时,说明 `T` 中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。 + +移动 `left` 收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。 + +至此,应该可以完全理解这套框架了,滑动窗口算法又不难,就是细节问题让人烦得很。**以后遇到滑动窗口算法,你就按照这框架写代码,保准没有 bug,还省事儿**。 + +下面就直接利用这套框架秒杀几道题吧,你基本上一眼就能看出思路了。 + + + + + + + +## 二、字符串排列 + +这是力扣第 567 题「字符串的排列」,难度中等: + + + +注意哦,输入的 `s1` 是可以包含重复字符的,所以这个题难度不小。 + +这种题目,是明显的滑动窗口算法,**相当给你一个 `S` 和一个 `T`,请问你 `S` 中是否存在一个子串,包含 `T` 中所有字符且不包含其他字符**? + +首先,先复制粘贴之前的算法框架代码,然后明确刚才提出的几个问题,即可写出这道题的答案: + +```java +class Solution { + // 判断 s 中是否存在 t 的排列 + public boolean checkInclusion(String t, String s) { + Map need = new HashMap<>(); + Map window = new HashMap<>(); + for (char c : t.toCharArray()) { + need.put(c, need.getOrDefault(c, 0) + 1); + } + + int left = 0, right = 0; + int valid = 0; + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).intValue() == need.get(c).intValue()) + valid++; + } + + // 判断左侧窗口是否要收缩 + while (right - left >= t.length()) { + // 在这里判断是否找到了合法的子串 + if (valid == need.size()) + return true; + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).intValue() == need.get(d).intValue()) + valid--; + window.put(d, window.get(d) - 1); + } + } + } + // 未找到符合条件的子串 + return false; + } +} +``` + + + +你可以点开下面的可视化面板,多次点击 while (right < s.length) 这一行代码,即可看到定长窗口滑动的过程: + + + +对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变几个地方: + +1、本题移动 `left` 缩小窗口的时机是窗口大小大于 `t.length()` 时,因为排列嘛,显然长度应该是一样的。 + +2、当发现 `valid == need.size()` 时,就说明窗口中就是一个合法的排列,所以立即返回 `true`。 + +至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。 + +> [!NOTE] +> 由于这道题中 `[left, right)` 其实维护的是一个**定长**的窗口,窗口长度为 `t.length()`。因为定长窗口每次向前滑动时只会移出一个字符,所以完全可以把内层的 while 改成 if,效果是一样的。 + + + + + + + +## 三、找所有字母异位词 + +这是力扣第 438 题「找到字符串中所有字母异位词」,难度中等: + + + +呵呵,这个所谓的字母异位词,不就是排列吗,搞个高端的说法就能糊弄人了吗?**相当于,输入一个串 `S`,一个串 `T`,找到 `S` 中所有 `T` 的排列,返回它们的起始索引**。 + +直接默写一下框架,明确刚才讲的 4 个问题,即可秒杀这道题: + +```java +class Solution { + public List findAnagrams(String s, String t) { + Map need = new HashMap<>(); + Map window = new HashMap<>(); + for (char c : t.toCharArray()) { + need.put(c, need.getOrDefault(c, 0) + 1); + } + + int left = 0, right = 0; + int valid = 0; + // 记录结果 + List res = new ArrayList<>(); + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(c)) { + window.put(c, window.getOrDefault(c, 0) + 1); + if (window.get(c).equals(need.get(c))) { + valid++; + } + } + // 判断左侧窗口是否要收缩 + while (right - left >= t.length()) { + // 当窗口符合条件时,把起始索引加入 res + if (valid == need.size()) + res.add(left); + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + if (need.containsKey(d)) { + if (window.get(d).equals(need.get(d))) { + valid--; + } + window.put(d, window.get(d) - 1); + } + } + } + return res; + } +} +``` + +跟寻找字符串的排列一样,只是找到一个合法异位词(排列)之后将起始索引加入 `res` 即可。 + + + +你可以点开下面的可视化面板,多次点击 while (right < s.length) 这一行代码,即可看到定长窗口滑动的过程: + + + +## 四、最长无重复子串 + +这是力扣第 3 题「无重复字符的最长子串」,难度中等: + + + +这个题终于有了点新意,不是一套框架就出答案,不过反而更简单了,稍微改一改框架就行了: + +```java +class Solution { + public int lengthOfLongestSubstring(String s) { + Map window = new HashMap<>(); + int left = 0, right = 0; + // 记录结果 + int res = 0; + while (right < s.length()) { + char c = s.charAt(right); + right++; + // 进行窗口内数据的一系列更新 + window.put(c, window.getOrDefault(c, 0) + 1); + // 判断左侧窗口是否要收缩 + while (window.get(c) > 1) { + char d = s.charAt(left); + left++; + // 进行窗口内数据的一系列更新 + window.put(d, window.get(d) - 1); + } + // 在这里更新答案 + res = Math.max(res, right - left); + } + return res; + } +} +``` + + + +你可以点开下面的可视化面板,多次点击 while (right < s.length) 这一行代码,即可看到窗口滑动更新答案的过程: + + + +这就是变简单了,连 `need` 和 `valid` 都不需要,而且更新窗口内数据也只需要简单的更新计数器 `window` 即可。 + +当 `window[c]` 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 `left` 缩小窗口了嘛。 + +唯一需要注意的是,在哪里更新结果 `res` 呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢? + +这里和之前不一样,要在收缩窗口完成后更新 `res`,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。 + +好了,滑动窗口算法模板就讲到这里,希望大家能理解其中的思想,记住算法模板并融会贯通。回顾一下,遇到子数组/子串相关的问题,你只要能回答出来以下几个问题,就能运用滑动窗口算法: + +1、什么时候应该扩大窗口? + +2、什么时候应该缩小窗口? + +3、什么时候应该更新答案? + +我在 [滑动窗口经典习题](https://labuladong.online/algo/problem-set/sliding-window/) 中使用这套思维模式列举了更多经典的习题,旨在强化你对算法的理解和记忆,以后就再也不怕子串、子数组问题了。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】前缀和技巧经典习题](https://labuladong.online/algo/problem-set/perfix-sum/) + - [【强化练习】单调队列的通用实现及经典习题](https://labuladong.online/algo/problem-set/monotonic-queue/) + - [【强化练习】滑动窗口算法经典习题](https://labuladong.online/algo/problem-set/sliding-window/) + - [动态规划设计:最大子数组](https://labuladong.online/algo/dynamic-programming/maximum-subarray/) + - [单调队列结构解决滑动窗口问题](https://labuladong.online/algo/data-structure/monotonic-queue/) + - [双指针技巧秒杀七道数组题目](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [拓展:归并排序详解及应用](https://labuladong.online/algo/practice-in-action/merge-sort/) + - [滑动窗口延伸:Rabin Karp 字符匹配算法](https://labuladong.online/algo/practice-in-action/rabinkarp/) + - [环形数组技巧](https://labuladong.online/algo/data-structure-basic/cycle-array/) + - [算法刷题的重点和坑](https://labuladong.online/algo/intro/how-to-learn-algorithms/) + - [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1004. Max Consecutive Ones III](https://leetcode.com/problems/max-consecutive-ones-iii/?show=1) | [1004. 最大连续1的个数 III](https://leetcode.cn/problems/max-consecutive-ones-iii/?show=1) | 🟠 | +| [1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit](https://leetcode.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/?show=1) | [1438. 绝对差不超过限制的最长连续子数组](https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/?show=1) | 🟠 | +| [1658. Minimum Operations to Reduce X to Zero](https://leetcode.com/problems/minimum-operations-to-reduce-x-to-zero/?show=1) | [1658. 将 x 减到 0 的最小操作数](https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/?show=1) | 🟠 | +| [209. Minimum Size Subarray Sum](https://leetcode.com/problems/minimum-size-subarray-sum/?show=1) | [209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/?show=1) | 🟠 | +| [219. Contains Duplicate II](https://leetcode.com/problems/contains-duplicate-ii/?show=1) | [219. 存在重复元素 II](https://leetcode.cn/problems/contains-duplicate-ii/?show=1) | 🟢 | +| [220. Contains Duplicate III](https://leetcode.com/problems/contains-duplicate-iii/?show=1) | [220. 存在重复元素 III](https://leetcode.cn/problems/contains-duplicate-iii/?show=1) | 🔴 | +| [340. Longest Substring with At Most K Distinct Characters](https://leetcode.com/problems/longest-substring-with-at-most-k-distinct-characters/?show=1)🔒 | [340. 至多包含 K 个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-k-distinct-characters/?show=1)🔒 | 🟠 | +| [395. Longest Substring with At Least K Repeating Characters](https://leetcode.com/problems/longest-substring-with-at-least-k-repeating-characters/?show=1) | [395. 至少有 K 个重复字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/?show=1) | 🟠 | +| [424. Longest Repeating Character Replacement](https://leetcode.com/problems/longest-repeating-character-replacement/?show=1) | [424. 替换后的最长重复字符](https://leetcode.cn/problems/longest-repeating-character-replacement/?show=1) | 🟠 | +| [560. Subarray Sum Equals K](https://leetcode.com/problems/subarray-sum-equals-k/?show=1) | [560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/?show=1) | 🟠 | +| [713. Subarray Product Less Than K](https://leetcode.com/problems/subarray-product-less-than-k/?show=1) | [713. 乘积小于 K 的子数组](https://leetcode.cn/problems/subarray-product-less-than-k/?show=1) | 🟠 | +| [862. Shortest Subarray with Sum at Least K](https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/?show=1) | [862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/?show=1) | 🔴 | +| - | [剑指 Offer 48. 最长不含重复字符的子字符串](https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/?show=1) | 🟠 | +| - | [剑指 Offer 57 - II. 和为s的连续正数序列](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/?show=1) | 🟢 | +| - | [剑指 Offer II 008. 和大于等于 target 的最短子数组](https://leetcode.cn/problems/2VG8Kg/?show=1) | 🟠 | +| - | [剑指 Offer II 009. 乘积小于 K 的子数组](https://leetcode.cn/problems/ZVAVXX/?show=1) | 🟠 | +| - | [剑指 Offer II 010. 和为 k 的子数组](https://leetcode.cn/problems/QTMn0o/?show=1) | 🟠 | +| - | [剑指 Offer II 014. 字符串中的变位词](https://leetcode.cn/problems/MPnaiL/?show=1) | 🟠 | +| - | [剑指 Offer II 015. 字符串中的所有变位词](https://leetcode.cn/problems/VabMRr/?show=1) | 🟠 | +| - | [剑指 Offer II 016. 不含重复字符的最长子字符串](https://leetcode.cn/problems/wtcaE1/?show=1) | 🟠 | +| - | [剑指 Offer II 017. 含有所有字符的最短字符串](https://leetcode.cn/problems/M1oyTv/?show=1) | 🔴 | +| - | [剑指 Offer II 057. 值和下标之差都在给定的范围内](https://leetcode.cn/problems/7WqeDu/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" index 79d363fb43..de77684e9f 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" @@ -1,20 +1,38 @@ -# 烧饼排序 +# 烧饼排序算法 -烧饼排序是个很有意思的实际问题:假设盘子上有 `n` 块**面积大小不一**的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)? -![](../pictures/pancakeSort/1.jpg) + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [969. Pancake Sorting](https://leetcode.com/problems/pancake-sorting/) | [969. 煎饼排序](https://leetcode.cn/problems/pancake-sorting/) | 🟠 | + +**-----------** + + + +力扣第 969 题「煎饼排序」是个很有意思的实际问题:假设盘子上有 `n` 块**面积大小不一**的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)? + +![](https://labuladong.online/algo/images/pancakeSort/1.jpg) 设想一下用锅铲翻转一堆烧饼的情景,其实是有一点限制的,我们每次只能将最上面的若干块饼子翻转: -![](../pictures/pancakeSort/2.png) +![](https://labuladong.online/algo/images/pancakeSort/2.png) 我们的问题是,**如何使用算法得到一个翻转序列,使得烧饼堆变得有序**? 首先,需要把这个问题抽象,用数组来表示烧饼堆: -![](../pictures/pancakeSort/title.png) + -如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md),这也是需要**递归思想**的。 +如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/),这也是需要**递归思想**的。 ### 一、思路分析 @@ -27,11 +45,11 @@ void sort(int[] cakes, int n); 如果我们找到了前 `n` 个烧饼中最大的那个,然后设法将这个饼子翻转到最底下: -![](../pictures/pancakeSort/3.jpg) +![](https://labuladong.online/algo/images/pancakeSort/3.jpg) 那么,原问题的规模就可以减小,递归调用 `pancakeSort(A, n-1)` 即可: -![](../pictures/pancakeSort/4.jpg) +![](https://labuladong.online/algo/images/pancakeSort/4.jpg) 接下来,对于上面的这 `n - 1` 块饼,如何排序呢?还是先从中找到最大的一块饼,然后把这块饼放到底下,再递归调用 `pancakeSort(A, n-1-1)`…… @@ -60,72 +78,92 @@ base case:`n == 1` 时,排序 1 个饼时不需要翻转。 只要把上述的思路用代码实现即可,唯一需要注意的是,数组索引从 0 开始,而我们要返回的结果是从 1 开始算的。 ```java -// 记录反转操作序列 -LinkedList res = new LinkedList<>(); +class Solution { + // 记录反转操作序列 + LinkedList res = new LinkedList<>(); -List pancakeSort(int[] cakes) { - sort(cakes, cakes.length); - return res; -} + public List pancakeSort(int[] cakes) { + sort(cakes, cakes.length); + return res; + } -void sort(int[] cakes, int n) { - // base case - if (n == 1) return; - - // 寻找最大饼的索引 - int maxCake = 0; - int maxCakeIndex = 0; - for (int i = 0; i < n; i++) - if (cakes[i] > maxCake) { - maxCakeIndex = i; - maxCake = cakes[i]; - } - - // 第一次翻转,将最大饼翻到最上面 - reverse(cakes, 0, maxCakeIndex); - res.add(maxCakeIndex + 1); - // 第二次翻转,将最大饼翻到最下面 - reverse(cakes, 0, n - 1); - res.add(n); - - // 递归调用 - sort(cakes, n - 1); -} + void sort(int[] cakes, int n) { + // base case + if (n == 1) return; + + // 寻找最大饼的索引 + int maxCake = 0; + int maxCakeIndex = 0; + for (int i = 0; i < n; i++) + if (cakes[i] > maxCake) { + maxCakeIndex = i; + maxCake = cakes[i]; + } + + // 第一次翻转,将最大饼翻到最上面 + reverse(cakes, 0, maxCakeIndex); + res.add(maxCakeIndex + 1); + // 第二次翻转,将最大饼翻到最下面 + reverse(cakes, 0, n - 1); + res.add(n); + + // 递归调用 + sort(cakes, n - 1); + } -void reverse(int[] arr, int i, int j) { - while (i < j) { - int temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; - i++; j--; + void reverse(int[] arr, int i, int j) { + while (i < j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; j--; + } } } ``` + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + 通过刚才的详细解释,这段代码应该是很清晰了。 算法的时间复杂度很容易计算,因为递归调用的次数是 `n`,每次递归调用都需要一次 for 循环,时间复杂度是 O(n),所以总的复杂度是 O(n^2)。 -**最后,我们可以思考一个问题​**:按照我们这个思路,得出的操作序列长度应该为​ `2(n - 1)`,因为每次递归都要进行 2 次翻转并记录操作,总共有 `n` 层递归,但由于 base case 直接返回结果,不进行翻转,所以最终的操作序列长度应该是固定的 `2(n - 1)`。 +**最后,我们可以思考一个问题**:按照我们这个思路,得出的操作序列长度应该为 `2(n - 1)`,因为每次递归都要进行 2 次翻转并记录操作,总共有 `n` 层递归,但由于 base case 直接返回结果,不进行翻转,所以最终的操作序列长度应该是固定的 `2(n - 1)`。 显然,这个结果不是最优的(最短的),比如说一堆煎饼 `[3,2,4,1]`,我们的算法得到的翻转序列是 `[3,4,2,3,1,2]`,但是最快捷的翻转方法应该是 `[2,3,4]`: +``` 初始状态 :[3,2,4,1] 翻前 2 个:[2,3,4,1] 翻前 3 个:[4,3,2,1] 翻前 4 个:[1,2,3,4] +``` 如果要求你的算法计算排序烧饼的**最短**操作序列,你该如何计算呢?或者说,解决这种求最优解法的问题,核心思路什么,一定需要使用什么算法技巧呢? 不妨分享一下你的思考。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:拆解复杂问题:实现计算器](../数据结构系列/实现计算器.md) -[下一篇:前缀和技巧](../算法思维系列/前缀和技巧.md) -[目录](../README.md#目录) \ No newline at end of file + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" deleted file mode 100644 index accfede4c4..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" +++ /dev/null @@ -1,87 +0,0 @@ -# 算法学习之路 - -之前发的那篇关于框架性思维的文章,我也发到了不少其他圈子,受到了大家的普遍好评,这一点我真的没想到,首先感谢大家的认可,我会更加努力,写出通俗易懂的算法文章。 - -有很多朋友问我数据结构和算法到底该怎么学,尤其是很多朋友说自己是「小白」,感觉这些东西好难啊,就算看了之前的「框架思维」,也感觉自己刷题乏力,希望我能聊聊我从一个非科班小白一路是怎么学过来的。 - -首先要给怀有这样疑问的朋友鼓掌,因为你现在已经「知道自己不知道」,而且开始尝试学习、刷题、寻求帮助,能做到这一点本身就是及其困难的。 - -关于「框架性思维」,对于一个小白来说,可能暂时无法完全理解(如果你能理解,说明你水平已经不错啦,不是小白啦)。就像软件工程,对于我这种没带过项目的人来说,感觉其内容枯燥乏味,全是废话,但是对于一个带过团队的人,他就会觉得软件工程里的每一句话都是精华。暂时不太理解没关系,留个印象,功夫到了很快就明白了。 - -下面写一写我一路过来的一些经验。如果你已经看过很多「如何高效刷题」「如何学习算法」的文章,却还是没有开始行动并坚持下去,本文的第五点就是写给你的。 - -我觉得之所以有时候认为自己是「小白」,是由于知识某些方面的空白造成的。具体到数据结构的学习,无非就是两个问题搞得不太清楚:**这是啥?有啥用?** - -举个例子,比如说你看到了「栈」这个名词,老师可能会讲这些关键词:先进后出、函数堆栈等等。但是,对于初学者,这些描述属于文学词汇,没有实际价值,没有解决最基本的两个问题。如何回答这两个基本问题呢?回答「这是啥」需要看教科书,回答「有啥用」需要刷算法题。 - -**一、这是啥?** - -这个问题最容易解决,就像一层窗户纸,你只要随便找本书看两天,自己动手实现一个「队列」「栈」之类的数据结构,就能捅破这层窗户纸。 - -这时候你就能理解「框架思维」文章中的前半部分了:数据结构无非就是数组、链表为骨架的一些特定操作而已;每个数据结构实现的功能无非增删查改罢了。 - -比如说「列队」这个数据结构,无非就是基于数组或者链表,实现 enqueue 和 dequeue 两个方法。这两个方法就是增和删呀,连查和改的方法都不需要。 - -**二、有啥用?** - -解决这个问题,就涉及算法的设计了,是个持久战,需要经常进行抽象思考,刷算法题,培养「计算机思维」。 - -之前的文章讲了,算法就是对数据结构准确而巧妙的运用。常用算法问题也就那几大类,算法题无非就是不断变换场景,给那几个算法框架套上不同的皮。刷题,就是在锻炼你的眼力,看你能不能看穿问题表象揪出相应的解法框架。 - -比如说,让你求解一个迷宫,你要把这个问题层层抽象:迷宫 -> 图的遍历 -> N 叉树的遍历 -> 二叉树的遍历。然后让框架指导你写具体的解法。 - -抽象问题,直击本质,是刷题中你需要刻意培养的能力。 - -**三、如何看书** - -直接推荐一本公认的好书,《算法第 4 版》,我一般简写成《算法4》。不要蜻蜓点水,这本书你能选择性的看上 50%,基本上就达到平均水平了。别怕这本书厚,因为起码有三分之一不用看,下面讲讲怎么看这本书。 - -看书仍然遵循递归的思想:自顶向下,逐步求精。 - -这本书知识结构合理,讲解也清楚,所以可以按顺序学习。**书中正文的算法代码一定要亲自敲一遍**,因为这些真的是扎实的基础,要认真理解。不要以为自己看一遍就看懂了,不动手的话理解不了的。但是,开头部分的基础可以酌情跳过;书中的数学证明,如不影响对算法本身的理解,完全可以跳过;章节最后的练习题,也可以全部跳过。这样一来,这本书就薄了很多。 - -相信读者现在已经认可了「框架性思维」的重要性,这种看书方式也是一种框架性策略,抓大放小,着重理解整体的知识架构,而忽略证明、练习题这种细节问题,即**保持自己对新知识的好奇心,避免陷入无限的细节被劝退。** - -当然,《算法4》到后面的内容也比较难了,比如那几个著名的串算法,以及正则表达式算法。这些属于「经典算法」,看个人接受能力吧,单说刷 LeetCode 的话,基本用不上,量力而行即可。 - -**四、如何刷题** - -首先声明一下,**算法和数学水平没关系,和编程语言也没关系**,你爱用什么语言用什么。算法,主要是培养一种新的思维方式。所谓「计算机思维」,就跟你考驾照一样,你以前骑自行车,有一套自行车的规则和技巧,现在你开汽车,就需要适应并练习开汽车的规则和技巧。 - -LeetCode 上的算法题和前面说的「经典算法」不一样,我们权且称为「解闷算法」吧,因为很多题目都比较有趣,有种在做奥数题或者脑筋急转弯的感觉。比如说,让你用队列实现一个栈,或者用栈实现一个队列,以及不用加号做加法,开脑洞吧? - -当然,这些问题虽然看起来无厘头,实际生活中也用不到,但是想解决这些问题依然要靠数据结构以及对基础知识的理解,也许这就是很多公司面试都喜欢出这种「智力题」的原因。下面说几点技巧吧。 - -**尽量刷英文版的 LeetCode**,中文版的“力扣”是阉割版,不仅很多题目没有答案,而且连个讨论区都没有。英文版的是真的很良心了,很多问题都有官方解答,详细易懂。而且讨论区(Discuss)也沉淀了大量优质内容,甚至好过官方解答。真正能打开你思路的,很可能是讨论区各路大神的思路荟萃。 - -PS:**如果有的英文题目实在看不懂,有个小技巧**,你在题目页面的 url 里加一个 -cn,即 https://leetcode.com/xxx 改成 https://leetcode-cn.com/xxx,这样就能切换到相应的中文版页面查看。 - -对于初学者,**强烈建议从 Explore 菜单里最下面的 Learn 开始刷**,这个专题就是专门教你学习数据结构和基本算法的,教学篇和相应的练习题结合,不要太良心。 - -最近 Learn 专题里新增了一些内容,我们挑数据结构相关的内容刷就行了,像 Ruby,Machine Learning 就没必要刷了。刷完 Learn 专题的基础内容,基本就有能力去 Explore 菜单的 Interview 专题刷面试题,或者去 Problem 菜单,在真正的题海里遨游了。 - -无论刷 Explore 还是 Problems 菜单,**最好一个分类一个分类的刷,不要蜻蜓点水**。比如说这几天就刷链表,刷完链表再去连刷几天二叉树。这样做是为了帮助你提取「框架」。一旦总结出针对一类问题的框架,解决同类问题可谓是手到擒来。 - -**五、道理我都懂,还是不能坚持下去** - -这其实无关算法了,还是老生常谈的执行力的问题。不说什么破鸡汤了,我觉得**解决办法就是「激起欲望」**,注意我说的是欲望,而不是常说的兴趣,拿我自己说说吧。 - -半年前我开始刷题,目的和大部分人都一样的,就是为毕业找工作做准备。只不过,大部分人是等到临近毕业了才开始刷,而我离毕业还有一阵子。这不是炫耀我多有觉悟,而是我承认自己的极度平凡。 - -首先,我真的想找到一份不错的工作(谁都想吧?),我想要高薪呀!否则我在朋友面前,女神面前放下的骚话,最终都会反过来啪啪地打我的脸。我也是要恰饭,要面子,要虚荣心的嘛。赚钱,虚荣心,足以激起我的欲望了。 - -但是,我不擅长 deadline 突击,我理解东西真的慢,所以干脆笨鸟先飞了。智商不够,拿时间来补,我没能力两个月突击,干脆拉长战线,打他个两年游击战,我还不信耗不死算法这个强敌。事实证明,你如果认真学习一个月,就能够取得肉眼可见的进步了。 - -现在,我依然在坚持刷题,而且为了另外一个原因,这个公众号。我没想到自己的文字竟然能够帮助到他人,甚至能得到认可。这也是虚荣心啊,我不能让读者失望啊,我想让更多的人认可(夸)我呀! - -以上,不光是坚持刷算法题吧,很多场景都适用。执行力是要靠「欲望」支撑的,我也是一凡人,只有那些看得见摸得着的东西才能使我快乐呀。读者不妨也尝试把刷题学习和自己的切身利益联系起来,这恐怕是坚持下去最简单直白的理由了。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) - -[上一篇:队列实现栈\|栈实现队列](../数据结构系列/队列实现栈栈实现队列.md) - -[下一篇:回溯算法详解](../算法思维系列/回溯算法详解修订版.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" new file mode 100644 index 0000000000..432e92915b --- /dev/null +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" @@ -0,0 +1,412 @@ +# 二维数组的花式遍历技巧 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [151. Reverse Words in a String](https://leetcode.com/problems/reverse-words-in-a-string/) | [151. 反转字符串中的单词](https://leetcode.cn/problems/reverse-words-in-a-string/) | 🟠 | +| [48. Rotate Image](https://leetcode.com/problems/rotate-image/) | [48. 旋转图像](https://leetcode.cn/problems/rotate-image/) | 🟠 | +| [54. Spiral Matrix](https://leetcode.com/problems/spiral-matrix/) | [54. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) | 🟠 | +| [59. Spiral Matrix II](https://leetcode.com/problems/spiral-matrix-ii/) | [59. 螺旋矩阵 II](https://leetcode.cn/problems/spiral-matrix-ii/) | 🟠 | +| [61. Rotate List](https://leetcode.com/problems/rotate-list/) | [61. 旋转链表](https://leetcode.cn/problems/rotate-list/) | 🟠 | +| - | [剑指 Offer 29. 顺时针打印矩阵](https://leetcode.cn/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组基础](https://labuladong.online/algo/data-structure-basic/array-basic/) + +有些读者说,看了本站的很多文章,掌握了框架思维,可以解决大部分有套路框架可循的题目。 + +但是框架思维也不是万能的,有一些特定技巧呢,属于会者不难,难者不会的类型,只能通过多刷题进行总结和积累。 + +那么本文我分享一些巧妙的二维数组的花式操作,你只要有个印象,以后遇到类似题目就不会懵圈了。 + +## 顺/逆时针旋转矩阵 + +对二维数组进行旋转是常见的笔试题,力扣第 48 题「旋转图像」就是很经典的一道: + + + +题目很好理解,就是让你将一个二维矩阵顺时针旋转 90 度,**难点在于要「原地」修改**,函数签名如下: + +```java +void rotate(int[][] matrix) +``` + +如何「原地」旋转二维矩阵?稍想一下,感觉操作起来非常复杂,可能要设置巧妙的算法机制来「一圈一圈」旋转矩阵: + +![](https://labuladong.online/algo/images/2d-array/1.png) + +**但实际上,这道题不能走寻常路**,在讲巧妙解法之前,我们先看另一道谷歌曾经考过的算法题热热身: + +给你一个包含若干单词和空格的字符串 `s`,请你写一个算法,**原地**反转所有单词的顺序。 + +比如说,给你输入这样一个字符串: + +```shell +s = "hello world labuladong" +``` + +你的算法需要**原地**反转这个字符串中的单词顺序: + +```shell +s = "labuladong world hello" +``` + +常规的方式是把 `s` 按空格 `split` 成若干单词,然后 `reverse` 这些单词的顺序,最后把这些单词 `join` 成句子。但这种方式使用了额外的空间,并不是「原地反转」单词。 + +**正确的做法是,先将整个字符串 `s` 反转**: + +```shell +s = "gnodalubal dlrow olleh" +``` + +**然后将每个单词分别反转**: + +```shell +s = "labuladong world hello" +``` + + + + + + + +这样,就实现了原地反转所有单词顺序的目的。力扣第 151 题「颠倒字符串中的单词」就是类似的问题,你可以顺便去做一下。 + +上面这个小技巧还可以再包装包装,比如说你可以去看一下力扣第 61 题「旋转链表」:给你一个单链表,让你旋转链表,将链表每个节点向右移动 `k` 个位置。 + +比如说输入单链表 `1 -> 2 -> 3 -> 4 -> 5`,`k = 2`,你的算法需要返回 `4 -> 5 -> 1 -> 2 -> 3`,即将链表每个节点向右移动 2 个位置。 + +这个题,不要真傻乎乎地一个一个去移动链表节点,我给你翻译翻译,其实就是将链表的后 `k` 个节点移动到链表的头部嘛,反应过来没有? + +还没反应过来,那再提示一下,把后 `k` 个节点移动到链表的头部,其实就是让你把链表的前 `n - k` 个节点和后 `k` 个节点原地翻转,对不对? + +这样,是不是和前面说的原地翻转字符串中的单词是一样的道理呢?你只需要先将整个链表反转,然后将前 `n - k` 个节点和后 `k` 个节点分别反转,就得到了结果。 + +当然,这个题有一些小细节,比如这个 `k` 可能大于链表的长度,那么你需要先求出链表的长度 `n`,然后取模 `k = k % n`,这样 `k` 就不会大于链表的长度,且最后得到的结果也是正确的。 + +有时间的话自己去做一下这个题吧,比较简单,我这里就不贴代码了。 + +我讲上面这两道题的目的是什么呢? + +**旨在说明,有时候咱们拍脑袋的常规思维,在计算机看来可能并不是最优雅的;但是计算机觉得最优雅的思维,对咱们来说却不那么直观**。也许这就是算法的魅力所在吧。 + + + + + + + +回到之前说的顺时针旋转二维矩阵的问题,常规的思路就是去寻找原始坐标和旋转后坐标的映射规律,但我们是否可以让思维跳跃跳跃,尝试把矩阵进行反转、镜像对称等操作,可能会出现新的突破口。 + +**我们可以先将 `n x n` 矩阵 `matrix` 按照左上到右下的对角线进行镜像对称**: + +![](https://labuladong.online/algo/images/2d-array/2.jpeg) + +**然后再对矩阵的每一行进行反转**: + +![](https://labuladong.online/algo/images/2d-array/3.jpeg) + +**发现结果就是 `matrix` 顺时针旋转 90 度的结果**: + +![](https://labuladong.online/algo/images/2d-array/4.jpeg) + +将上述思路翻译成代码,即可解决本题: + +```java +class Solution { + // 将二维矩阵原地顺时针旋转 90 度 + public void rotate(int[][] matrix) { + int n = matrix.length; + // 先沿对角线镜像对称二维矩阵 + for (int i = 0; i < n; i++) { + for (int j = i; j < n; j++) { + // swap(matrix[i][j], matrix[j][i]); + int temp = matrix[i][j]; + matrix[i][j] = matrix[j][i]; + matrix[j][i] = temp; + } + } + // 然后反转二维矩阵的每一行 + for (int[] row : matrix) { + reverse(row); + } + } + + // 反转一维数组 + void reverse(int[] arr) { + int i = 0, j = arr.length - 1; + while (j > i) { + // swap(arr[i], arr[j]); + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; + j--; + } + } +} +``` + + + +你可以打开下面的可视化面板,多次点击 let temp = matrix[i][j] 这行代码,即可看到对角线翻转的过程;然后再多次点击 reverse(row) 这行代码,即可看到每一行被反转,得到最终答案: + + + +肯定有读者会问,如果没有做过这道题,怎么可能想到这种思路呢? + +是的,没做过这类题目,确实不好想到这种思路,但你这不是做过了么?所谓会者不难难者不会,你这辈子估计都忘不掉了。 + +**既然说道这里,我们可以发散一下,如何将矩阵逆时针旋转 90 度呢**? + +思路是类似的,只要通过另一条对角线镜像对称矩阵,然后再反转每一行,就得到了逆时针旋转矩阵的结果: + +![](https://labuladong.online/algo/images/2d-array/5.jpeg) + +翻译成代码如下: + +```java +class Solution { + + // 将二维矩阵原地逆时针旋转 90 度 + public void rotate2(int[][] matrix) { + int n = matrix.length; + // 沿左下到右上的对角线镜像对称二维矩阵 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n - i; j++) { + // swap(matrix[i][j], matrix[n-j-1][n-i-1]) + int temp = matrix[i][j]; + matrix[i][j] = matrix[n - j - 1][n - i - 1]; + matrix[n - j - 1][n - i - 1] = temp; + } + } + // 然后反转二维矩阵的每一行 + for (int[] row : matrix) { + reverse(row); + } + } + + void reverse(int[] arr) { + // 见上文 + } +} +``` + +至此,旋转矩阵的问题就解决了。 + +## 矩阵的螺旋遍历 + +接下来我们讲一下力扣第 54 题「螺旋矩阵」,看一看二维矩阵可以如何花式遍历: + + + + + + + +```java +// 函数签名如下 +List spiralOrder(int[][] matrix) +``` + +**解题的核心思路是按照右、下、左、上的顺序遍历数组,并使用四个变量圈定未遍历元素的边界**: + +![](https://labuladong.online/algo/images/2d-array/6.png) + +随着螺旋遍历,相应的边界会收缩,直到螺旋遍历完整个数组: + +![](https://labuladong.online/algo/images/2d-array/7.png) + +只要有了这个思路,翻译出代码就很容易了: + +```java +class Solution { + public List spiralOrder(int[][] matrix) { + int m = matrix.length, n = matrix[0].length; + int upper_bound = 0, lower_bound = m - 1; + int left_bound = 0, right_bound = n - 1; + List res = new LinkedList<>(); + // res.size() == m * n 则遍历完整个数组 + while (res.size() < m * n) { + if (upper_bound <= lower_bound) { + // 在顶部从左向右遍历 + for (int j = left_bound; j <= right_bound; j++) { + res.add(matrix[upper_bound][j]); + } + // 上边界下移 + upper_bound++; + } + + if (left_bound <= right_bound) { + // 在右侧从上向下遍历 + for (int i = upper_bound; i <= lower_bound; i++) { + res.add(matrix[i][right_bound]); + } + // 右边界左移 + right_bound--; + } + + if (upper_bound <= lower_bound) { + // 在底部从右向左遍历 + for (int j = right_bound; j >= left_bound; j--) { + res.add(matrix[lower_bound][j]); + } + // 下边界上移 + lower_bound--; + } + + if (left_bound <= right_bound) { + // 在左侧从下向上遍历 + for (int i = lower_bound; i >= upper_bound; i--) { + res.add(matrix[i][left_bound]); + } + // 左边界右移 + left_bound++; + } + } + return res; + } +} +``` + + + +你可以打开下面的可视化面板,多次点击 while (res.length < m * n) 这行代码,即可看到由外向内螺旋遍历的过程: + + + +力扣第 59 题「螺旋矩阵 II」也是类似的题目,只不过是反过来,让你按照螺旋的顺序生成矩阵: + + + + + + + +```java +// 函数签名如下 +int[][] generateMatrix(int n) +``` + +有了上面的铺垫,稍微改一下代码即可完成这道题: + +```java +class Solution { + public int[][] generateMatrix(int n) { + int[][] matrix = new int[n][n]; + int upper_bound = 0, lower_bound = n - 1; + int left_bound = 0, right_bound = n - 1; + // 需要填入矩阵的数字 + int num = 1; + + while (num <= n * n) { + if (upper_bound <= lower_bound) { + // 在顶部从左向右遍历 + for (int j = left_bound; j <= right_bound; j++) { + matrix[upper_bound][j] = num++; + } + // 上边界下移 + upper_bound++; + } + + if (left_bound <= right_bound) { + // 在右侧从上向下遍历 + for (int i = upper_bound; i <= lower_bound; i++) { + matrix[i][right_bound] = num++; + } + // 右边界左移 + right_bound--; + } + + if (upper_bound <= lower_bound) { + // 在底部从右向左遍历 + for (int j = right_bound; j >= left_bound; j--) { + matrix[lower_bound][j] = num++; + } + // 下边界上移 + lower_bound--; + } + + if (left_bound <= right_bound) { + // 在左侧从下向上遍历 + for (int i = lower_bound; i >= upper_bound; i--) { + matrix[i][left_bound] = num++; + } + // 左边界右移 + left_bound++; + } + } + return matrix; + } +} +``` + + + +你可以打开下面的可视化面板,多次点击 while (num <= n * n) 这行代码,即可看到生成螺旋矩阵的过程: + + + +至此,两道螺旋矩阵的题目也解决了。 + +以上就是遍历二维数组的一些技巧,其他数组技巧可参见之前的文章 [前缀和数组](https://labuladong.online/algo/data-structure/prefix-sum/),[差分数组](https://labuladong.online/algo/data-structure/diff-array/),[数组双指针算法集合](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/),链表相关技巧可参见 [单链表六大算法技巧汇总](https://labuladong.online/algo/essential-technique/linked-list-skills-summary/)。 + + + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】数组双指针经典习题](https://labuladong.online/algo/problem-set/array-two-pointers/) + - [双指针技巧秒杀七道数组题目](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1260. Shift 2D Grid](https://leetcode.com/problems/shift-2d-grid/?show=1) | [1260. 二维网格迁移](https://leetcode.cn/problems/shift-2d-grid/?show=1) | 🟢 | +| [1329. Sort the Matrix Diagonally](https://leetcode.com/problems/sort-the-matrix-diagonally/?show=1) | [1329. 将矩阵按对角线排序](https://leetcode.cn/problems/sort-the-matrix-diagonally/?show=1) | 🟠 | +| [867. Transpose Matrix](https://leetcode.com/problems/transpose-matrix/?show=1) | [867. 转置矩阵](https://leetcode.cn/problems/transpose-matrix/?show=1) | 🟢 | +| - | [剑指 Offer 58 - I. 翻转单词顺序](https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof/?show=1) | 🟢 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" deleted file mode 100644 index 1e49bc9ac9..0000000000 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" +++ /dev/null @@ -1,261 +0,0 @@ -# 递归详解 - -首先说明一个问题,简单阐述一下递归,分治算法,动态规划,贪心算法这几个东西的区别和联系,心里有个印象就好。 - -递归是一种编程技巧,一种解决问题的思维方式;分治算法和动态规划很大程度上是递归思想基础上的(虽然动态规划的最终版本大都不是递归了,但解题思想还是离不开递归),解决更具体问题的两类算法思想;贪心算法是动态规划算法的一个子集,可以更高效解决一部分更特殊的问题。 - -分治算法将在这节讲解,以最经典的归并排序为例,它把待排序数组不断二分为规模更小的子问题处理,这就是 “分而治之” 这个词的由来。显然,排序问题分解出的子问题是不重复的,如果有的问题分解后的子问题有重复的(重叠子问题性质),那么就交给动态规划算法去解决! - -## 递归详解 - -介绍分治之前,首先要弄清楚递归这个概念。 - -递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题,而递归是把问题逐级分解,是纵向的拆分。 - -以下会举例说明我对递归的一点理解,**如果你不想看下去了,请记住这几个问题怎么回答:** - -1. 如何给一堆数字排序? 答:分成两半,先排左半边再排右半边,最后合并就行了,至于怎么排左边和右边,请重新阅读这句话。 -2. 孙悟空身上有多少根毛? 答:一根毛加剩下的毛。 -3. 你今年几岁? 答:去年的岁数加一岁,1999 年我出生。 - -递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。 - -```c++ -int func(你今年几岁) { - // 最简子问题,结束条件 - if (你1999年几岁) return 我0岁; - // 自我调用,缩小规模 - return func(你去年几岁) + 1; -} -``` - -其实仔细想想,**递归运用最成功的是什么?我认为是数学归纳法。**我们高中都学过数学归纳法,使用场景大概是:我们推不出来某个求和公式,但是我们试了几个比较小的数,似乎发现了一点规律,然后编了一个公式,看起来应该是正确答案。但是数学是很严谨的,你哪怕穷举了一万个数都是正确的,但是第一万零一个数正确吗?这就要数学归纳法发挥神威了,可以假设我们编的这个公式在第 k 个数时成立,如果证明在第 k + 1 时也成立,那么我们编的这个公式就是正确的。 - -那么数学归纳法和递归有什么联系?我们刚才说了,递归代码必须要有结束条件,如果没有的话就会进入无穷无尽的自我调用,直到内存耗尽。而数学证明的难度在于,你可以尝试有穷种情况,但是难以将你的结论延伸到无穷大。这里就可以看出联系了 —— 无穷。 - -递归代码的精髓在于调用自己去解决规模更小的子问题,直到到达结束条件;而数学归纳法之所以有用,就在于不断把我们的猜测向上加一,扩大结论的规模,没有结束条件,从而把结论延伸到无穷无尽,也就完成了猜测正确性的证明。 - -### 为什么要写递归 - -首先为了训练逆向思考的能力。递推的思维是正常人的思维,总是看着眼前的问题思考对策,解决问题是将来时;递归的思维,逼迫我们倒着思考,看到问题的尽头,把解决问题的过程看做过去时。 - -第二,练习分析问题的结构,当问题可以被分解成相同结构的小问题时,你能敏锐发现这个特点,进而高效解决问题。 - -第三,跳出细节,从整体上看问题。再说说归并排序,其实可以不用递归来划分左右区域的,但是代价就是代码极其难以理解,大概看一下代码(归并排序在后面讲,这里大致看懂意思就行,体会递归的妙处): - -```java -void sort(Comparable[] a){ - int N = a.length; - // 这么复杂,是对排序的不尊重。我拒绝研究这样的代码。 - for (int sz = 1; sz < N; sz = sz + sz) - for (int lo = 0; lo < N - sz; lo += sz + sz) - merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); -} - -/* 我还是选择递归,简单,漂亮 */ -void sort(Comparable[] a, int lo, int hi) { - if (lo >= hi) return; - int mid = lo + (hi - lo) / 2; - sort(a, lo, mid); // 排序左半边 - sort(a, mid + 1, hi); // 排序右半边 - merge(a, lo, mid, hi); // 合并两边 -} - -``` - -看起来简洁漂亮是一方面,关键是**可解释性很强**:把左半边排序,把右半边排序,最后合并两边。而非递归版本看起来不知所云,充斥着各种难以理解的边界计算细节,特别容易出 bug 且难以调试,人生苦短,我更倾向于递归版本。 - -显然有时候递归处理是高效的,比如归并排序,**有时候是低效的**,比如数孙悟空身上的毛,因为堆栈会消耗额外空间,而简单的递推不会消耗空间。比如这个例子,给一个链表头,计算它的长度: - -```java -/* 典型的递推遍历框架,需要额外空间 O(1) */ -public int size(Node head) { - int size = 0; - for (Node p = head; p != null; p = p.next) size++; - return size; -} -/* 我偏要递归,万物皆递归,需要额外空间 O(N) */ -public int size(Node head) { - if (head == null) return 0; - return size(head.next) + 1; -} -``` - -### 写递归的技巧 - -我的一点心得是:**明白一个函数的作用并相信它能完成这个任务,千万不要试图跳进细节。**千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。 - -先举个最简单的例子:遍历二叉树。 - -```cpp -void traverse(TreeNode* root) { - if (root == nullptr) return; - traverse(root->left); - traverse(root->right); -} -``` - -这几行代码就足以扫荡任何一棵二叉树了。我想说的是,对于递归函数`traverse(root)`,我们只要相信:给它一个根节点`root`,它就能遍历这棵树,因为写这个函数不就是为了这个目的吗?所以我们只需要把这个节点的左右节点再甩给这个函数就行了,因为我相信它能完成任务的。那么遍历一棵N叉数呢?太简单了好吧,和二叉树一模一样啊。 - -```cpp -void traverse(TreeNode* root) { - if (root == nullptr) return; - for (child : root->children) - traverse(child); -} -``` - -至于遍历的什么前、中、后序,那都是显而易见的,对于N叉树,显然没有中序遍历。 - - - -以下**详解 LeetCode 的一道题来说明**:给一课二叉树,和一个目标值,节点上的值有正有负,返回树中和等于目标值的路径条数,让你编写 pathSum 函数: - -``` -/* 来源于 LeetCode PathSum III: https://leetcode.com/problems/path-sum-iii/ */ -root = [10,5,-3,3,2,null,11,3,-2,null,1], -sum = 8 - - 10 - / \ - 5 -3 - / \ \ - 3 2 11 - / \ \ -3 -2 1 - -Return 3. The paths that sum to 8 are: - -1. 5 -> 3 -2. 5 -> 2 -> 1 -3. -3 -> 11 -``` - -```cpp -/* 看不懂没关系,底下有更详细的分析版本,这里突出体现递归的简洁优美 */ -int pathSum(TreeNode root, int sum) { - if (root == null) return 0; - return count(root, sum) + - pathSum(root.left, sum) + pathSum(root.right, sum); -} -int count(TreeNode node, int sum) { - if (node == null) return 0; - return (node.val == sum) + - count(node.left, sum - node.val) + count(node.right, sum - node.val); -} -``` - -题目看起来很复杂吧,不过代码却极其简洁,这就是递归的魅力。我来简单总结这个问题的**解决过程**: - -首先明确,递归求解树的问题必然是要遍历整棵树的,所以**二叉树的遍历框架**(分别对左右孩子递归调用函数本身)必然要出现在主函数 pathSum 中。那么对于每个节点,他们应该干什么呢?他们应该看看,自己和脚底下的小弟们包含多少条符合条件的路径。好了,这道题就结束了。 - -按照前面说的技巧,根据刚才的分析来定义清楚每个递归函数应该做的事: - -PathSum 函数:给他一个节点和一个目标值,他返回以这个节点为根的树中,和为目标值的路径总数。 - -count 函数:给他一个节点和一个目标值,他返回以这个节点为根的树中,能凑出几个以该节点为路径开头,和为目标值的路径总数。 - -```cpp -/* 有了以上铺垫,详细注释一下代码 */ -int pathSum(TreeNode root, int sum) { - if (root == null) return 0; - int pathImLeading = count(root, sum); // 自己为开头的路径数 - int leftPathSum = pathSum(root.left, sum); // 左边路径总数(相信他能算出来) - int rightPathSum = pathSum(root.right, sum); // 右边路径总数(相信他能算出来) - return leftPathSum + rightPathSum + pathImLeading; -} -int count(TreeNode node, int sum) { - if (node == null) return 0; - // 我自己能不能独当一面,作为一条单独的路径呢? - int isMe = (node.val == sum) ? 1 : 0; - // 左边的小老弟,你那边能凑几个 sum - node.val 呀? - int leftBrother = count(node.left, sum - node.val); - // 右边的小老弟,你那边能凑几个 sum - node.val 呀? - int rightBrother = count(node.right, sum - node.val); - return isMe + leftBrother + rightBrother; // 我这能凑这么多个 -} -``` - -还是那句话,明白每个函数能做的事,并相信他们能够完成。 - -总结下,PathSum 函数提供的二叉树遍历框架,在遍历中对每个节点调用 count 函数,看出先序遍历了吗(这道题什么序都是一样的);count 函数也是一个二叉树遍历,用于寻找以该节点开头的目标值路径。好好体会吧! - -## 分治算法 - -**归并排序**,典型的分治算法;分治,典型的递归结构。 - -分治算法可以分三步走:分解 -> 解决 -> 合并 - -1. 分解原问题为结构相同的子问题。 -2. 分解到某个容易求解的边界之后,进行第归求解。 -3. 将子问题的解合并成原问题的解。 - -归并排序,我们就叫这个函数`merge_sort`吧,按照我们上面说的,要明确该函数的职责,即**对传入的一个数组排序**。OK,那么这个问题能不能分解呢?当然可以!给一个数组排序,不就等于给该数组的两半分别排序,然后合并就完事了。 - -```cpp -void merge_sort(一个数组) { - if (可以很容易处理) return; - merge_sort(左半个数组); - merge_sort(右半个数组); - merge(左半个数组, 右半个数组); -} -``` - -好了,这个算法也就这样了,完全没有任何难度。记住之前说的,相信函数的能力,传给他半个数组,那么这半个数组就已经被排好了。而且你会发现这不就是个二叉树遍历模板吗?为什么是后序遍历?因为我们分治算法的套路是 **分解 -> 解决(触底) -> 合并(回溯)** 啊,先左右分解,再处理合并,回溯就是在退栈,就相当于后序遍历了。至于`merge`函数,参考两个有序链表的合并,简直一模一样,下面直接贴代码吧。 - -下面参考《算法4》的 Java 代码,很漂亮。由此可见,不仅算法思想思想重要,编码技巧也是挺重要的吧!多思考,多模仿。 - -```java -public class Merge { - // 不要在 merge 函数里构造新数组了,因为 merge 函数会被多次调用,影响性能 - // 直接一次性构造一个足够大的数组,简洁,高效 - private static Comparable[] aux; - - public static void sort(Comparable[] a) { - aux = new Comparable[a.length]; - sort(a, 0, a.length - 1); - } - - private static void sort(Comparable[] a, int lo, int hi) { - if (lo >= hi) return; - int mid = lo + (hi - lo) / 2; - sort(a, lo, mid); - sort(a, mid + 1, hi); - merge(a, lo, mid, hi); - } - - private static void merge(Comparable[] a, int lo, int mid, int hi) { - int i = lo, j = mid + 1; - for (int k = lo; k <= hi; k++) - aux[k] = a[k]; - for (int k = lo; k <= hi; k++) { - if (i > mid) { a[k] = aux[j++]; } - else if (j > hi) { a[k] = aux[i++]; } - else if (less(aux[j], aux[i])) { a[k] = aux[j++]; } - else { a[k] = aux[i++]; } - } - } - - private static boolean less(Comparable v, Comparable w) { - return v.compareTo(w) < 0; - } -} -``` - -LeetCode 上有分治算法的专项练习,可复制到浏览器去做题: - -https://leetcode.com/tag/divide-and-conquer/ - - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:洗牌算法](../算法思维系列/洗牌算法.md) - -[下一篇:如何实现LRU算法](../高频面试系列/LRU算法.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" new file mode 100644 index 0000000000..1ce0d45b04 --- /dev/null +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" @@ -0,0 +1,601 @@ +# 回溯算法实践:集合划分 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [698. Partition to K Equal Sum Subsets](https://leetcode.com/problems/partition-to-k-equal-sum-subsets/) | [698. 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [多叉树结构及遍历框架](https://labuladong.online/algo/data-structure-basic/n-ary-tree-traverse-basic/) +> - [二叉树系列算法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) +> - [回溯算法框架套路](https://labuladong.online/algo/essential-technique/backtrack-framework/) +> - [球盒模型:回溯算法的两种穷举视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) + + +我之前说过回溯算法是笔试中最好用的算法,只要你没什么思路,就用回溯算法暴力求解,即便不能通过所有测试用例,多少能过一点。回溯算法的技巧也不算难,就是穷举一棵决策树的过程,只要在递归之前「做选择」,在递归之后「撤销选择」就行了。 + +**但是,就算暴力穷举,不同的思路也有优劣之分**。本文就来看一道非常经典的回溯算法问题,力扣第 698 题「划分为k个相等的子集」。这道题可以帮你更深刻理解回溯算法的思维,得心应手地写出回溯函数。 + +题目非常简单: + +给你输入一个数组 `nums` 和一个正整数 `k`,请你判断 `nums` 是否能够被平分为元素和相同的 `k` 个子集。 + +函数签名如下: + +```java +boolean canPartitionKSubsets(int[] nums, int k); +``` + +> [!NOTE] +> 我们之前 [背包问题之子集划分](https://labuladong.online/algo/dynamic-programming/knapsack2/) 写过一次子集划分问题,不过那道题只需要我们把集合划分成两个相等的集合,可以转化成背包问题用动态规划技巧解决。 +> +> 为什么划分成两个相等的子集可以转化成背包问题用动态规划思路解决,而划分成 `k` 个相等的子集就不可以转化成背包问题,只能用回溯算法暴力穷举?请先尝试自己思考。 + +> [!NOTE] +> 为什么划分两个相等的子集可以转化成背包问题? +> +> [背包问题之子集划分](https://labuladong.online/algo/dynamic-programming/knapsack2/) 的场景中,有一个背包和若干物品,每个物品有**两个选择**,分别是「装进背包」和「不装进背包」。把原集合 `S` 划分成两个相等子集 `S_1, S_2` 的场景下,`S` 中的每个元素也有**两个选择**,分别是「装进 `S_1`」和「不装进 `S_1`(装进 `S_2`)」,这时候的穷举思路其实和背包问题相同。 +> +> 但如果你想把 `S` 划分成 `k` 个相等的子集,相当于 `S` 中的每个元素有 **`k` 个选择**,这和标准背包问题的场景有本质区别,是无法套用背包问题的解题思路的。 + + + + + + + +## 题目思路 + +回到正题,这道算法题让我们求子集划分,子集问题和排列组合问题有所区别,但我们可以借鉴「球盒模型」的抽象,用两种不同的视角来解决这道子集划分问题。 + +把装有 `n` 个数字的数组 `nums` 分成 `k` 个和相同的集合,你可以想象将 `n` 个数字分配到 `k` 个「桶」里,最后这 `k` 个「桶」里的数字之和要相同。 + +前文 [用球盒模型理解回溯算法](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) 说过,回溯算法的关键在哪里? + +关键是要知道怎么「做选择」,这样才能利用递归函数进行穷举。 + +那么模仿排列公式的推导思路,将 `n` 个数字分配到 `k` 个桶里,我们也可以有两种视角: + +**视角一,如果我们切换到这 `n` 个数字的视角,每个数字都要选择进入到 `k` 个桶中的某一个**。 + +![](https://labuladong.online/algo/images/set-split/5.jpeg) + +**视角二,如果我们切换到这 `k` 个桶的视角,对于每个桶,都要遍历 `nums` 中的 `n` 个数字,然后选择是否将当前遍历到的数字装进自己这个桶里**。 + +![](https://labuladong.online/algo/images/set-split/6.jpeg) + +你可能问,这两种视角有什么不同? + +和前面讲的排列子集类似,用不同的视角进行穷举,虽然结果相同,但是解法代码的逻辑不同,具体的代码实现也不同,可能产生不同的时间、空间复杂度。我们需要选择复杂度更低的解法。 + +## 以数字的视角 + +用 for 循环迭代遍历 `nums` 数组大家肯定都会: + + + + + +```java +for (int index = 0; index < nums.length; index++) { + print(nums[index]); +} +``` + + + +递归遍历数组你会不会?其实也很简单: + + + + + +```java +void traverse(int[] nums, int index) { + if (index == nums.length) { + return; + } + print(nums[index]); + traverse(nums, index + 1); +} +``` + + + +只要调用 `traverse(nums, 0)`,和 for 循环的效果是完全一样的。 + +那么回到这道题,以数字的视角,选择 `k` 个桶,用 for 循环写出来是下面这样: + +```java +// k 个桶(集合),记录每个桶装的数字之和 +int[] bucket = new int[k]; + +// 穷举 nums 中的每个数字 +for (int index = 0; index < nums.length; index++) { + // 穷举每个桶 + for (int i = 0; i < k; i++) { + // nums[index] 选择是否要进入第 i 个桶 + // ... + } +} +``` + +如果改成递归的形式,就是下面这段代码逻辑: + +```java +// k 个桶(集合),记录每个桶装的数字之和 +int[] bucket = new int[k]; + +// 穷举 nums 中的每个数字 +void backtrack(int[] nums, int index) { + // base case + if (index == nums.length) { + return; + } + // 穷举每个桶 + for (int i = 0; i < bucket.length; i++) { + // 选择装进第 i 个桶 + bucket[i] += nums[index]; + // 递归穷举下一个数字的选择 + backtrack(nums, index + 1); + // 撤销选择 + bucket[i] -= nums[index]; + } +} +``` + +虽然上述代码仅仅是穷举逻辑,还不能解决我们的问题,但是只要略加完善即可: + +```java +class Solution { + public boolean canPartitionKSubsets(int[] nums, int k) { + // 排除一些基本情况 + if (k > nums.length) return false; + int sum = 0; + for (int v : nums) sum += v; + if (sum % k != 0) return false; + + // k 个桶(集合),记录每个桶装的数字之和 + int[] bucket = new int[k]; + // 理论上每个桶(集合)中数字的和 + int target = sum / k; + // 穷举,看看 nums 是否能划分成 k 个和为 target 的子集 + return backtrack(nums, 0, bucket, target); + } + + // 递归穷举 nums 中的每个数字 + boolean backtrack( + int[] nums, int index, int[] bucket, int target) { + + if (index == nums.length) { + // 检查所有桶的数字之和是否都是 target + for (int i = 0; i < bucket.length; i++) { + if (bucket[i] != target) { + return false; + } + } + // nums 成功平分成 k 个子集 + return true; + } + + // 穷举 nums[index] 可能装入的桶 + for (int i = 0; i < bucket.length; i++) { + // 剪枝,桶装装满了 + if (bucket[i] + nums[index] > target) { + continue; + } + // 将 nums[index] 装入 bucket[i] + bucket[i] += nums[index]; + // 递归穷举下一个数字的选择 + if (backtrack(nums, index + 1, bucket, target)) { + return true; + } + // 撤销选择 + bucket[i] -= nums[index]; + } + + // nums[index] 装入哪个桶都不行 + return false; + } +} +``` + +有之前的铺垫,相信这段代码是比较容易理解的,其实我们可以再做一个优化。 + +主要看 `backtrack` 函数的递归部分: + +```java +for (int i = 0; i < bucket.length; i++) { + // 剪枝 + if (bucket[i] + nums[index] > target) { + continue; + } + + if (backtrack(nums, index + 1, bucket, target)) { + return true; + } +} +``` + +**如果我们让尽可能多的情况命中剪枝的那个 if 分支,就可以减少递归调用的次数,一定程度上减少时间复杂度**。 + +如何尽可能多的命中这个 if 分支呢?要知道我们的 `index` 参数是从 0 开始递增的,也就是递归地从 0 开始遍历 `nums` 数组。 + +如果我们提前对 `nums` 数组排序,把大的数字排在前面,那么大的数字会先被分配到 `bucket` 中,对于之后的数字,`bucket[i] + nums[index]` 会更大,更容易触发剪枝的 if 条件。 + +所以可以在之前的代码中再添加一些代码: + +```java +boolean canPartitionKSubsets(int[] nums, int k) { + // 其他代码不变 + // ... + // 降序排序 nums 数组 + Arrays.sort(nums); + // 翻转数组,得到降序数组 + for (i = 0, j = nums.length - 1; i < j; i++, j--) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + // ***************** + return backtrack(nums, 0, bucket, target); +} +``` + +这个解法可以得到正确答案,但耗时比较多,已经无法通过所有测试用例了,接下来看看另一种视角的解法。 + +## 以桶的视角 + +文章开头说了,**以桶的视角进行穷举,每个桶需要遍历 `nums` 中的所有数字,决定是否把当前数字装进桶中;当装满一个桶之后,还要装下一个桶,直到所有桶都装满为止**。 + +这个思路可以用下面这段代码表示出来: + +```java +// 装满所有桶为止 +while (k > 0) { + // 记录当前桶中的数字之和 + int bucket = 0; + for (int i = 0; i < nums.length; i++) { + // 决定是否将 nums[i] 放入当前桶中 + if (canAdd(bucket, num[i])) { + bucket += nums[i]; + } + if (bucket == target) { + // 装满了一个桶,装下一个桶 + k--; + break; + } + } +} +``` + +那么我们也可以把这个 while 循环改写成递归函数,不过比刚才略微复杂一些,首先写一个 `backtrack` 递归函数出来: + +```java +boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int target); +``` + +不要被这么多参数吓到,我会一个个解释这些参数。如果你够透彻理解了前文 [用球盒模型理解回溯算法](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/),也能得心应手地写出这样的回溯函数。 + +这个 `backtrack` 函数的参数可以这样解释: + +现在 `k` 号桶正在思考是否应该把 `nums[start]` 这个元素装进来;目前 `k` 号桶里面已经装的数字之和为 `bucket`;`used` 标志某一个元素是否已经被装到桶中;`target` 是每个桶需要达成的目标和。 + +根据这个函数定义,可以这样调用 `backtrack` 函数: + +```java +class Solution { + public boolean canPartitionKSubsets(int[] nums, int k) { + // 排除一些基本情况 + if (k > nums.length) return false; + int sum = 0; + for (int v : nums) sum += v; + if (sum % k != 0) return false; + + boolean[] used = new boolean[nums.length]; + int target = sum / k; + // k 号桶初始什么都没装,从 nums[0] 开始做选择 + return backtrack(k, 0, nums, 0, used, target); + } +} +``` + +实现 `backtrack` 函数的逻辑之前,再重复一遍,从桶的视角: + +1、需要遍历 `nums` 中所有数字,决定哪些数字需要装到当前桶中。 + +2、如果当前桶装满了(桶内数字和达到 `target`),则让下一个桶开始执行第 1 步。 + +下面的代码就实现了这个逻辑: + +```java +class Solution { + public boolean canPartitionKSubsets(int[] nums, int k) { + // 见上文 + } + + boolean backtrack(int k, int bucket, + int[] nums, int start, boolean[] used, int target) { + // base case + if (k == 0) { + // 所有桶都被装满了,而且 nums 一定全部用完了 + // 因为 target == sum / k + return true; + } + if (bucket == target) { + // 装满了当前桶,递归穷举下一个桶的选择 + // 让下一个桶从 nums[0] 开始选数字 + return backtrack(k - 1, 0 ,nums, 0, used, target); + } + + // 从 start 开始向后探查有效的 nums[i] 装入当前桶 + for (int i = start; i < nums.length; i++) { + // 剪枝 + if (used[i]) { + // nums[i] 已经被装入别的桶中 + continue; + } + if (nums[i] + bucket > target) { + // 当前桶装不下 nums[i] + continue; + } + // 做选择,将 nums[i] 装入当前桶中 + used[i] = true; + bucket += nums[i]; + // 递归穷举下一个数字是否装入当前桶 + if (backtrack(k, bucket, nums, i + 1, used, target)) { + return true; + } + // 撤销选择 + used[i] = false; + bucket -= nums[i]; + } + // 穷举了所有数字,都无法装满当前桶 + return false; + } +} +``` + +**这段代码是可以得出正确答案的,但是效率很低,我们可以思考一下是否还有优化的空间**。 + +首先,在这个解法中每个桶都可以认为是没有差异的,但是我们的回溯算法却会对它们区别对待,这里就会出现重复计算的情况。 + +什么意思呢?我们的回溯算法,说到底就是穷举所有可能的组合,然后看是否能找出和为 `target` 的 `k` 个桶(子集)。 + +那么,比如下面这种情况,`target = 5`,算法会在第一个桶里面装 `1, 4`: + +![](https://labuladong.online/algo/images/set-split/1.jpeg) + +现在第一个桶装满了,就开始装第二个桶,算法会装入 `2, 3`: + +![](https://labuladong.online/algo/images/set-split/2.jpeg) + +然后以此类推,对后面的元素进行穷举,凑出若干个和为 5 的桶(子集)。 + +但问题是,如果最后发现无法凑出和为 `target` 的 `k` 个子集,算法会怎么做? + +回溯算法会回溯到第一个桶,重新开始穷举,现在它知道第一个桶里装 `1, 4` 是不可行的,它会尝试把 `2, 3` 装到第一个桶里: + +![](https://labuladong.online/algo/images/set-split/3.jpeg) + +现在第一个桶装满了,就开始装第二个桶,算法会装入 `1, 4`: + +![](https://labuladong.online/algo/images/set-split/4.jpeg) + +好,到这里你应该看出来问题了,这种情况其实和之前的那种情况是一样的。也就是说,到这里你其实已经知道不需要再穷举了,必然凑不出来和为 `target` 的 `k` 个子集。 + +但我们的算法还是会傻乎乎地继续穷举,因为在她看来,第一个桶和第二个桶里面装的元素不一样,那这就是两种不一样的情况呀。 + +那么我们怎么让算法的智商提高,识别出这种情况,避免冗余计算呢? + +你注意这两种情况的 `used` 数组肯定长得一样,所以 `used` 数组可以认为是回溯过程中的「状态」。 + +**所以,我们可以用一个 `memo` 备忘录,在装满一个桶时记录当前 `used` 的状态,如果当前 `used` 的状态是曾经出现过的,那就不用再继续穷举,从而起到剪枝避免冗余计算的作用**。 + +有读者肯定会问,`used` 是一个布尔数组,怎么作为键进行存储呢?这其实是小问题,比如我们可以把数组转化成字符串,这样就可以作为哈希表的键进行存储了。 + +看下代码实现,只要稍微改一下 `backtrack` 函数即可: + +```java +class Solution { + + // 备忘录,存储 used 数组的状态 + HashMap memo = new HashMap<>(); + + public boolean canPartitionKSubsets(int[] nums, int k) { + // 见上文 + } + + boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int target) { + // base case + if (k == 0) { + return true; + } + // 将 used 的状态转化成形如 [true, false, ...] 的字符串 + // 便于存入 HashMap + String state = Arrays.toString(used); + + if (bucket == target) { + // 装满了当前桶,递归穷举下一个桶的选择 + boolean res = backtrack(k - 1, 0, nums, 0, used, target); + // 将当前状态和结果存入备忘录 + memo.put(state, res); + return res; + } + + if (memo.containsKey(state)) { + // 如果当前状态曾今计算过,就直接返回,不要再递归穷举了 + return memo.get(state); + } + + // 其他逻辑不变... + } +} +``` + +这样提交解法,发现执行效率依然比较低,这次不是因为算法逻辑上的冗余计算,而是代码实现上的问题。 + +**因为每次递归都要把 `used` 数组转化成字符串,这对于编程语言来说也是一个不小的消耗,所以我们还可以进一步优化**。 + +注意题目给的数据规模 `nums.length <= 16`,也就是说 `used` 数组最多也不会超过 16,那么我们完全可以用「位图」的技巧,用一个 int 类型的 `used` 变量来替代 `used` 数组。 + +具体来说,我们可以用整数 `used` 的第 `i` 位(`(used >> i) & 1`)的 1/0 来表示 `used[i]` 的 true/false。 + +这样一来,不仅节约了空间,而且整数 `used` 也可以直接作为键存入 HashMap,省去数组转字符串的消耗。 + +看下最终的解法代码: + +```java +class Solution { + public boolean canPartitionKSubsets(int[] nums, int k) { + // 排除一些基本情况 + if (k > nums.length) return false; + int sum = 0; + for (int v : nums) sum += v; + if (sum % k != 0) return false; + + // 使用位图技巧 + int used = 0; + int target = sum / k; + // k 号桶初始什么都没装,从 nums[0] 开始做选择 + return backtrack(k, 0, nums, 0, used, target); + } + + HashMap memo = new HashMap<>(); + + boolean backtrack(int k, int bucket, + int[] nums, int start, int used, int target) { + // base case + if (k == 0) { + // 所有桶都被装满了,而且 nums 一定全部用完了 + return true; + } + if (bucket == target) { + // 装满了当前桶,递归穷举下一个桶的选择 + // 让下一个桶从 nums[0] 开始选数字 + boolean res = backtrack(k - 1, 0, nums, 0, used, target); + // 缓存结果 + memo.put(used, res); + return res; + } + + if (memo.containsKey(used)) { + // 避免冗余计算 + return memo.get(used); + } + + for (int i = start; i < nums.length; i++) { + // 剪枝 + // 判断第 i 位是否是 1 + if (((used >> i) & 1) == 1) { + // nums[i] 已经被装入别的桶中 + continue; + } + if (nums[i] + bucket > target) { + continue; + } + // 做选择 + // 将第 i 位置为 1 + used |= 1 << i; + bucket += nums[i]; + // 递归穷举下一个数字是否装入当前桶 + if (backtrack(k, bucket, nums, i + 1, used, target)) { + return true; + } + // 撤销选择 + // 使用异或运算将第 i 位恢复 0 + used ^= 1 << i; + bucket -= nums[i]; + } + + return false; + } +} +``` + + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + +至此,这道题的第二种思路也完成了。 + +## 最后总结 + +本文写的这两种思路都可以算出正确答案,不过第一种解法即便经过了排序优化,也明显比第二种解法慢很多,这是为什么呢? + +我们来分析一下这两个算法的时间复杂度,假设 `nums` 中的元素个数为 `n`。 + +先说第一个解法,也就是从数字的角度进行穷举,`n` 个数字,每个数字有 `k` 个桶可供选择,所以组合出的结果个数为 `k^n`,时间复杂度也就是 $O(k^n)$。 + +第二个解法,每个桶要遍历 `n` 个数字,对每个数字有「装入」或「不装入」两种选择,所以组合的结果有 `2^n` 种;而我们有 `k` 个桶,所以总的时间复杂度为 $O(k*2^n)$。 + +**当然,这是对最坏复杂度上界的粗略估算,实际的复杂度肯定要好很多,毕竟我们添加了这么多剪枝逻辑**。不过,从复杂度的上界已经可以看出第一种思路要慢很多了。 + +所以,谁说回溯算法没有技巧性的?虽然回溯算法就是暴力穷举,但穷举也分聪明的穷举方式和低效的穷举方式,关键看你以谁的「视角」进行穷举。 + +通俗来说,我们应该尽量「少量多次」,就是说宁可多做几次选择(乘法关系),也不要给太大的选择空间(指数关系);做 `n` 次「`k` 选一」仅重复一次($O(k^n)$),比 `n` 次「二选一」重复 `k` 次($O(k*2^n)$)效率低很多。 + +好了,这道题我们从两种视角进行穷举,虽然代码量看起来多,但核心逻辑都是类似的,相信你通过本文能够更深刻地理解回溯算法。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】回溯算法经典习题 I](https://labuladong.online/algo/problem-set/backtrack-i/) + - [谁能想到,斗地主也能玩出算法](https://labuladong.online/algo/practice-in-action/split-array-into-consecutive-subsequences/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [473. Matchsticks to Square](https://leetcode.com/problems/matchsticks-to-square/?show=1) | [473. 火柴拼正方形](https://leetcode.cn/problems/matchsticks-to-square/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" index 64de979756..db28dcd7ac 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" @@ -1,8 +1,32 @@ -# LRU算法详解 +# 算法就像搭乐高:手撸 LRU 算法 -### 一、什么是 LRU 算法 -就是一种缓存淘汰策略。 + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [146. LRU Cache](https://leetcode.com/problems/lru-cache/) | [146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [链表基础](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) +> - [哈希表基础](https://labuladong.online/algo/data-structure-basic/hashmap-basic/) + +LRU 算法就是一种缓存淘汰策略,原理不难,但是面试中写出没有 bug 的算法比较有技巧,需要对数据结构进行层层抽象和拆解,本文就带你写一手漂亮的代码。 + +LRU 算法用到的关键数据结构是哈希链表 `LinkedHashMap`,数据结构基础章节的 [手把手带你实现哈希链表](https://labuladong.online/algo/data-structure-basic/hashtable-with-linked-list/) 专门讲解了哈希链表的原理及代码实现。如果你没看过也没关系,本文会再次讲解哈希链表的核心原理,以便实现 LRU 算法。 计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢? @@ -10,28 +34,36 @@ LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently 举个简单的例子,安卓手机都可以把软件放到后台运行,比如我先后打开了「设置」「手机管家」「日历」,那么现在他们在后台排列的顺序是这样的: -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/1.jpg) +![](https://labuladong.online/algo/images/lru/1.jpg) 但是这时候如果我访问了一下「设置」界面,那么「设置」就会被提前到第一个,变成这样: -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/2.jpg) +![](https://labuladong.online/algo/images/lru/2.jpg) 假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢? 按照 LRU 的策略,就关最底下的「手机管家」,因为那是最久未使用的,然后把新开的应用放到最上面: -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/3.jpg) +![](https://labuladong.online/algo/images/lru/3.jpg) + +现在你应该理解 LRU(Least Recently Used)策略了。当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景。本文讲解 LRU 算法策略,我会在 [LFU 算法详解](https://labuladong.online/algo/frequency-interview/lfu/) 中讲解 LFU 算法。 + + + + -现在你应该理解 LRU(Least Recently Used)策略了。当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景。本文讲解 LRU 算法策略。 -### 二、LRU 算法描述 -LRU 算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。 +## 一、LRU 算法描述 -注意哦,get 和 put 方法必须都是 $O(1)$ 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。 +力扣第 146 题「LRU缓存机制」就是让你设计数据结构: -```cpp -/* 缓存容量为 2 */ +首先要接收一个 `capacity` 参数作为缓存的最大容量,然后实现两个 API,一个是 `put(key, val)` 方法存入键值对,另一个是 `get(key)` 方法获取 `key` 对应的 `val`,如果 `key` 不存在则返回 -1。 + +注意哦,`get` 和 `put` 方法必须都是 $O(1)$ 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。 + +```java +// 缓存容量为 2 LRUCache cache = new LRUCache(2); // 你可以把 cache 理解成一个队列 // 假设左边是队头,右边是队尾 @@ -40,49 +72,72 @@ LRUCache cache = new LRUCache(2); cache.put(1, 1); // cache = [(1, 1)] + cache.put(2, 2); // cache = [(2, 2), (1, 1)] -cache.get(1); // 返回 1 + +// 返回 1 +cache.get(1); // cache = [(1, 1), (2, 2)] // 解释:因为最近访问了键 1,所以提前至队头 // 返回键 1 对应的值 1 + cache.put(3, 3); // cache = [(3, 3), (1, 1)] // 解释:缓存容量已满,需要删除内容空出位置 // 优先删除久未使用的数据,也就是队尾的数据 // 然后把新的数据插入队头 -cache.get(2); // 返回 -1 (未找到) + +// 返回 -1 (未找到) +cache.get(2); // cache = [(3, 3), (1, 1)] // 解释:cache 中不存在键为 2 的数据 + cache.put(1, 4); // cache = [(1, 4), (3, 3)] // 解释:键 1 已存在,把原始值 1 覆盖为 4 // 不要忘了也要将键值对提前到队头 ``` -### 三、LRU 算法设计 +## 二、LRU 算法设计 + +分析上面的操作过程,要让 `put` 和 `get` 方法的时间复杂度为 O(1),我们可以总结出 `cache` 这个数据结构必要的条件: + +1、显然 `cache` 中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。 + +2、我们要在 `cache` 中快速找某个 `key` 是否已存在并得到对应的 `val`; + +3、每次访问 `cache` 中的某个 `key`,需要将这个元素变为最近使用的,也就是说 `cache` 要支持在任意位置快速插入和删除元素。 + +那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 `LinkedHashMap`。 + + + -分析上面的操作过程,要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分。 -因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。 -那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。 LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样: -![HashLinkedList](../pictures/LRU%E7%AE%97%E6%B3%95/4.jpg) +![](https://labuladong.online/algo/images/lru/4.jpg) -思想很简单,就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。回想刚才的例子,这种数据结构是不是完美解决了 LRU 缓存的需求? +借助这个结构,我们来逐一分析上面的 3 个条件: -也许读者会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 key,为什么链表中还要存键值对呢,只存值不就行了? +1、如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。 + +2、对于某一个 `key`,我们可以通过哈希表快速定位到链表中的节点,从而取得对应 `val`。 + +3、链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 `key` 快速映射到任意一个链表节点,然后进行插入和删除。 + +**也许读者会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 `key`,为什么链表中还要存 `key` 和 `val` 呢,只存 `val` 不就行了**? 想的时候都是问题,只有做的时候才有答案。这样设计的原因,必须等我们亲自实现 LRU 算法之后才能理解,所以我们开始看代码吧~ -### 四、代码实现 +## 三、代码实现 -很多编程语言都有内置的哈希链表或者类似 LRU 功能的库函数,但是为了帮大家理解算法的细节,我们用 Java 自己造轮子实现一遍 LRU 算法。 +很多编程语言都有内置的哈希链表或者类似 LRU 功能的库函数,但是为了帮大家理解算法的细节,我们先自己造轮子实现一遍 LRU 算法,然后再使用 Java 内置的 `LinkedHashMap` 来实现一遍。 -首先,我们把双链表的节点类写出来,为了简化,key 和 val 都认为是 int 类型: +首先,我们把 [双链表](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) 的节点类写出来,为了简化,`key` 和 `val` 都认为是 int 类型: ```java class Node { @@ -95,65 +150,241 @@ class Node { } ``` -然后依靠我们的 Node 类型构建一个双链表,实现几个需要的 API(这些操作的时间复杂度均为 $O(1)$): +然后依靠我们的 `Node` 类型构建一个双链表,实现几个 LRU 算法必须的 API: ```java class DoubleList { - // 在链表头部添加节点 x,时间 O(1) - public void addFirst(Node x); + // 头尾虚节点 + private Node head, tail; + // 链表元素数 + private int size; + + public DoubleList() { + // 初始化双向链表的数据 + head = new Node(0, 0); + tail = new Node(0, 0); + head.next = tail; + tail.prev = head; + size = 0; + } + + // 在链表尾部添加节点 x,时间 O(1) + public void addLast(Node x) { + x.prev = tail.prev; + x.next = tail; + tail.prev.next = x; + tail.prev = x; + size++; + } // 删除链表中的 x 节点(x 一定存在) // 由于是双链表且给的是目标 Node 节点,时间 O(1) - public void remove(Node x); - - // 删除链表中最后一个节点,并返回该节点,时间 O(1) - public Node removeLast(); + public void remove(Node x) { + x.prev.next = x.next; + x.next.prev = x.prev; + size--; + } + // 删除链表中第一个节点,并返回该节点,时间 O(1) + public Node removeFirst() { + if (head.next == tail) + return null; + Node first = head.next; + remove(first); + return first; + } + // 返回链表长度,时间 O(1) - public int size(); + public int size() { return size; } + } ``` -PS:这就是普通双向链表的实现,为了让读者集中精力理解 LRU 算法的逻辑,就省略链表的具体代码。 +如果对链表的操作不熟悉,可以看前文 [手把手带你实现双链表](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/)。 + +到这里就能回答刚才「为什么必须要用双向链表」的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。 -到这里就能回答刚才“为什么必须要用双向链表”的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 $O(1)$。 +> [!IMPORTANT] +> 注意我们实现的双链表 API 只能从尾部插入,也就是说靠尾部的数据是最近使用的,靠头部的数据是最久未使用的。 -有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可。我们先把逻辑理清楚: +有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可,先搭出代码框架: ```java -// key 映射到 Node(key, val) -HashMap map; -// Node(k1, v1) <-> Node(k2, v2)... -DoubleList cache; - -int get(int key) { - if (key 不存在) { - return -1; - } else { - 将数据 (key, val) 提到开头; - return val; +class LRUCache { + // key -> Node(key, val) + private HashMap map; + // Node(k1, v1) <-> Node(k2, v2)... + private DoubleList cache; + // 最大容量 + private int cap; + + public LRUCache(int capacity) { + this.cap = capacity; + map = new HashMap<>(); + cache = new DoubleList(); } } +``` + +先不慌去实现 LRU 算法的 `get` 和 `put` 方法。由于我们要同时维护一个双链表 `cache` 和一个哈希表 `map`,很容易漏掉一些操作,比如说删除某个 `key` 时,在 `cache` 中删除了对应的 `Node`,但是却忘记在 `map` 中删除 `key`。 + +**解决这种问题的有效方法是:在这两种数据结构之上提供一层抽象 API**。 -void put(int key, int val) { - Node x = new Node(key, val); - if (key 已存在) { - 把旧的数据删除; - 将新节点 x 插入到开头; - } else { - if (cache 已满) { - 删除链表的最后一个数据腾位置; - 删除 map 中映射到该数据的键; - } - 将新节点 x 插入到开头; - map 中新建 key 对新节点 x 的映射; +就是尽量让 LRU 的主方法 `get` 和 `put` 避免直接操作 `map` 和 `cache` 的细节。我们可以先实现下面几个函数: + +```java +class LRUCache { + // 为了节约篇幅,省略上文给出的代码部分... + + // 将某个 key 提升为最近使用的 + private void makeRecently(int key) { + Node x = map.get(key); + // 先从链表中删除这个节点 + cache.remove(x); + // 重新插到队尾 + cache.addLast(x); + } + + // 添加最近使用的元素 + private void addRecently(int key, int val) { + Node x = new Node(key, val); + // 链表尾部就是最近使用的元素 + cache.addLast(x); + // 别忘了在 map 中添加 key 的映射 + map.put(key, x); + } + + // 删除某一个 key + private void deleteKey(int key) { + Node x = map.get(key); + // 从链表中删除 + cache.remove(x); + // 从 map 中删除 + map.remove(key); + } + + // 删除最久未使用的元素 + private void removeLeastRecently() { + // 链表头部的第一个元素就是最久未使用的 + Node deletedNode = cache.removeFirst(); + // 同时别忘了从 map 中删除它的 key + int deletedKey = deletedNode.key; + map.remove(deletedKey); } } ``` -如果能够看懂上述逻辑,翻译成代码就很容易理解了: +这里就能回答之前的问答题「为什么要在链表中同时存储 key 和 val,而不是只存储 val」,注意 `removeLeastRecently` 函数中,我们需要用 `deletedNode` 得到 `deletedKey`。 + +也就是说,当缓存容量已满,我们不仅仅要删除最后一个 `Node` 节点,还要把 `map` 中映射到该节点的 `key` 同时删除,而这个 `key` 只能由 `Node` 得到。如果 `Node` 结构中只存储 `val`,那么我们就无法得知 `key` 是什么,就无法删除 `map` 中的键,造成错误。 + +上述方法就是简单的操作封装,调用这些函数可以避免直接操作 `cache` 链表和 `map` 哈希表,下面我先来实现 LRU 算法的 `get` 方法: ```java +class LRUCache { + // 为了节约篇幅,省略上文给出的代码部分... + + public int get(int key) { + if (!map.containsKey(key)) { + return -1; + } + // 将该数据提升为最近使用的 + makeRecently(key); + return map.get(key).val; + } +} +``` + +`put` 方法稍微复杂一些,我们先来画个图搞清楚它的逻辑: + +![](https://labuladong.online/algo/images/lru/put.jpg) + +这样我们可以轻松写出 `put` 方法的代码: + +```java +class LRUCache { + // 为了节约篇幅,省略上文给出的代码部分... + + public void put(int key, int val) { + if (map.containsKey(key)) { + // 删除旧的数据 + deleteKey(key); + // 新插入的数据为最近使用的数据 + addRecently(key, val); + return; + } + + if (cap == cache.size()) { + // 删除最久未使用的元素 + removeLeastRecently(); + } + // 添加为最近使用的元素 + addRecently(key, val); + } +} +``` + +至此,你应该已经完全掌握 LRU 算法的原理和实现了。看下完整的实现: + +```java +// 双向链表节点 +class Node { + public int key, val; + public Node next, prev; + public Node(int k, int v) { + this.key = k; + this.val = v; + } +} + +// 双向链表 +class DoubleList { + // 头尾虚节点 + private Node head, tail; + // 链表元素数 + private int size; + + public DoubleList() { + // 初始化双向链表的数据 + head = new Node(0, 0); + tail = new Node(0, 0); + head.next = tail; + tail.prev = head; + size = 0; + } + + // 在链表尾部添加节点 x,时间 O(1) + public void addLast(Node x) { + x.prev = tail.prev; + x.next = tail; + tail.prev.next = x; + tail.prev = x; + size++; + } + + // 删除链表中的 x 节点(x 一定存在) + // 由于是双链表且给的是目标 Node 节点,时间 O(1) + public void remove(Node x) { + x.prev.next = x.next; + x.next.prev = x.prev; + size--; + } + + // 删除链表中第一个节点,并返回该节点,时间 O(1) + public Node removeFirst() { + if (head.next == tail) + return null; + Node first = head.next; + remove(first); + return first; + } + + // 返回链表长度,时间 O(1) + public int size() { return size; } + +} + + class LRUCache { // key -> Node(key, val) private HashMap map; @@ -169,58 +400,151 @@ class LRUCache { } public int get(int key) { - if (!map.containsKey(key)) + if (!map.containsKey(key)) { return -1; - int val = map.get(key).val; - // 利用 put 方法把该数据提前 - put(key, val); - return val; + } + // 将该数据提升为最近使用的 + makeRecently(key); + return map.get(key).val; } public void put(int key, int val) { - // 先把新节点 x 做出来 - Node x = new Node(key, val); - if (map.containsKey(key)) { - // 删除旧的节点,新的插到头部 - cache.remove(map.get(key)); - cache.addFirst(x); - // 更新 map 中对应的数据 - map.put(key, x); - } else { - if (cap == cache.size()) { - // 删除链表最后一个数据 - Node last = cache.removeLast(); - map.remove(last.key); - } - // 直接添加到头部 - cache.addFirst(x); - map.put(key, x); + // 删除旧的数据 + deleteKey(key); + // 新插入的数据为最近使用的数据 + addRecently(key, val); + return; + } + + if (cap == cache.size()) { + // 删除最久未使用的元素 + removeLeastRecently(); } + // 添加为最近使用的元素 + addRecently(key, val); + } + + private void makeRecently(int key) { + Node x = map.get(key); + // 先从链表中删除这个节点 + cache.remove(x); + // 重新插到队尾 + cache.addLast(x); + } + + private void addRecently(int key, int val) { + Node x = new Node(key, val); + // 链表尾部就是最近使用的元素 + cache.addLast(x); + // 别忘了在 map 中添加 key 的映射 + map.put(key, x); + } + + private void deleteKey(int key) { + Node x = map.get(key); + // 从链表中删除 + cache.remove(x); + // 从 map 中删除 + map.remove(key); + } + + private void removeLeastRecently() { + // 链表头部的第一个元素就是最久未使用的 + Node deletedNode = cache.removeFirst(); + // 同时别忘了从 map 中删除它的 key + int deletedKey = deletedNode.key; + map.remove(deletedKey); } } ``` -这里就能回答之前的问答题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”,注意这段代码: +你也可以用 Java 的内置类型 `LinkedHashMap` 或者 [手把手带你实现哈希链表](https://labuladong.online/algo/data-structure-basic/hashtable-with-linked-list/) 实现的 `MyLinkedHashMap` 来实现 LRU 算法,逻辑和之前完全一致: ```java -if (cap == cache.size()) { - // 删除链表最后一个数据 - Node last = cache.removeLast(); - map.remove(last.key); +class LRUCache { + int cap; + LinkedHashMap cache = new LinkedHashMap<>(); + public LRUCache(int capacity) { + this.cap = capacity; + } + + public int get(int key) { + if (!cache.containsKey(key)) { + return -1; + } + // 将 key 变为最近使用 + makeRecently(key); + return cache.get(key); + } + + public void put(int key, int val) { + if (cache.containsKey(key)) { + // 修改 key 的值 + cache.put(key, val); + // 将 key 变为最近使用 + makeRecently(key); + return; + } + + if (cache.size() >= this.cap) { + // 链表头部就是最久未使用的 key + int oldestKey = cache.keySet().iterator().next(); + cache.remove(oldestKey); + } + // 将新的 key 添加链表尾部 + cache.put(key, val); + } + + private void makeRecently(int key) { + int val = cache.get(key); + // 删除 key,重新插入到队尾 + cache.remove(key); + cache.put(key, val); + } } ``` -当缓存容量已满,我们不仅仅要删除最后一个 Node 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。 -至此,你应该已经掌握 LRU 算法的思想和实现了,很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: +至此,LRU 算法就没有什么神秘的了。更多数据结构设计相关的题目参见 [数据结构设计经典习题](https://labuladong.online/algo/problem-set/ds-design/)。 + + + + + + + +
+
+引用本文的文章 + + - [一文读懂 session 和 cookie](https://labuladong.online/algo/fname.html?fname=session和cookie) + - [算法就像搭乐高:手撸 LFU 算法](https://labuladong.online/algo/frequency-interview/lfu/) + - [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer II 031. 最近最少使用缓存](https://leetcode.cn/problems/OrIXps/?show=1) | 🟠 | + +
+
+ + -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:二叉堆详解实现优先级队列](../数据结构系列/二叉堆详解实现优先级队列.md) -[下一篇:二叉搜索树操作集锦](../数据结构系列/二叉搜索树操作集锦.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" index 5c31a12770..3e9823392d 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" @@ -2,6 +2,6 @@ 8 说了,本章都是高频面试题,配合前面的动态规划系列,祝各位马到成功! -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" deleted file mode 100644 index 13b32db912..0000000000 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" +++ /dev/null @@ -1,149 +0,0 @@ -# 如何运用二分查找算法 - -二分查找到底有能运用在哪里? - -最常见的就是教科书上的例子,在**有序数组**中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。 - -PS:以上提到的三种二分查找算法形式在前文「二分查找详解」有代码详解,如果没看过强烈建议看看。 - -抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。 - -说起来玄乎得很,本文先用一个具体的「Koko 吃香蕉」的问题来举个例子。 - -### 一、问题分析 - -![](../pictures/二分应用/title1.png) - -也就是说,Koko 每小时最多吃一堆香蕉,如果吃不下的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。在这个条件下,让我们确定 Koko 吃香蕉的**最小速度**(根/小时)。 - -如果直接给你这个情景,你能想到哪里能用到二分查找算法吗?如果没有见过类似的问题,恐怕是很难把这个问题和二分查找联系起来的。 - -那么我们先抛开二分查找技巧,想想如何暴力解决这个问题呢? - -首先,算法要求的是「`H` 小时内吃完香蕉的最小速度」,我们不妨称为 `speed`,请问 `speed` 最大可能为多少,最少可能为多少呢? - -显然最少为 1,最大为 `max(piles)`,因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1 开始穷举到 `max(piles)`,一旦发现发现某个值可以在 `H` 小时内吃完所有香蕉,这个值就是最小速度: - -```java -int minEatingSpeed(int[] piles, int H) { - // piles 数组的最大值 - int max = getMax(piles); - for (int speed = 1; speed < max; speed++) { - // 以 speed 是否能在 H 小时内吃完香蕉 - if (canFinish(piles, speed, H)) - return speed; - } - return max; -} -``` - -注意这个 for 循环,就是在**连续的空间线性搜索,这就是二分查找可以发挥作用的标志**。由于我们要求的是最小速度,所以可以用一个**搜索左侧边界的二分查找**来代替线性搜索,提升效率: - -```java -int minEatingSpeed(int[] piles, int H) { - // 套用搜索左侧边界的算法框架 - int left = 1, right = getMax(piles) + 1; - while (left < right) { - // 防止溢出 - int mid = left + (right - left) / 2; - if (canFinish(piles, mid, H)) { - right = mid; - } else { - left = mid + 1; - } - } - return left; -} -``` - -PS:如果对于这个二分查找算法的细节问题有疑问,建议看下前文「二分查找详解」搜索左侧边界的算法模板,这里不展开了。 - -剩下的辅助函数也很简单,可以一步步拆解实现: - -```java -// 时间复杂度 O(N) -boolean canFinish(int[] piles, int speed, int H) { - int time = 0; - for (int n : piles) { - time += timeOf(n, speed); - } - return time <= H; -} - -int timeOf(int n, int speed) { - return (n / speed) + ((n % speed > 0) ? 1 : 0); -} - -int getMax(int[] piles) { - int max = 0; - for (int n : piles) - max = Math.max(n, max); - return max; -} -``` - -至此,借助二分查找技巧,算法的时间复杂度为 O(NlogN)。 - -### 二、扩展延伸 - -类似的,再看一道运输问题: - -![](../pictures/二分应用/title2.png) - -要在 `D` 天内运输完所有货物,货物不可分割,如何确定运输的最小载重呢(下文称为 `cap`)? - -其实本质上和 Koko 吃香蕉的问题一样的,首先确定 `cap` 的最小值和最大值分别为 `max(weights)` 和 `sum(weights)`。 - -我们要求**最小载重**,所以可以用搜索左侧边界的二分查找算法优化线性搜索: - -```java -// 寻找左侧边界的二分查找 -int shipWithinDays(int[] weights, int D) { - // 载重可能的最小值 - int left = getMax(weights); - // 载重可能的最大值 + 1 - int right = getSum(weights) + 1; - while (left < right) { - int mid = left + (right - left) / 2; - if (canFinish(weights, D, mid)) { - right = mid; - } else { - left = mid + 1; - } - } - return left; -} - -// 如果载重为 cap,是否能在 D 天内运完货物? -boolean canFinish(int[] w, int D, int cap) { - int i = 0; - for (int day = 0; day < D; day++) { - int maxCap = cap; - while ((maxCap -= w[i]) >= 0) { - i++; - if (i == w.length) - return true; - } - } - return false; -} -``` - -通过这两个例子,你是否明白了二分查找在实际问题中的应用? - -```java -for (int i = 0; i < n; i++) - if (isOK(i)) - return ans; -``` - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:如何计算编辑距离](../动态规划系列/编辑距离.md) - -[下一篇:如何高效解决接雨水问题](../高频面试系列/接雨水.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" index 60088e5977..62688a4d7e 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" @@ -1,42 +1,65 @@ # 如何k个一组反转链表 -之前的文章「递归反转链表的一部分」讲了如何递归地反转一部分链表,有读者就问如何迭代地反转链表,这篇文章解决的问题也需要反转链表的函数,我们不妨就用迭代方式来解决。 +

+GitHub + + + +

-本文要解决「K 个一组反转链表」,不难理解: +![](https://labuladong.online/algo/images/souyisou1.png) -![](../pictures/kgroup/title.png) +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -这个问题经常在面经中看到,而且 LeetCode 上难度是 Hard,它真的有那么难吗? + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/) | [25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) | 🔴 + +**-----------** + +之前的文章 [递归反转链表的一部分](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/) 讲了如何递归地反转一部分链表,有读者就问如何迭代地反转链表,那么这篇文章的第一部分就会讲一讲如何用迭代方式解决反转单链表的问题。 + +有了这个反转函数之后,我们还是会用递归的方式解决力扣第 25 题「K 个一组翻转链表」,所以检验你递归思维的时候到了,准备好了吗? + +先看下题目,不难理解: + + + +这个问题经常在面经中看到,而且力扣上难度是 Hard,它真的有那么难吗? 对于基本数据结构的算法问题其实都不难,只要结合特点一点点拆解分析,一般都没啥难点。下面我们就来拆解一下这个问题。 ### 一、分析问题 -首先,前文[学习数据结构的框架思维](../算法思维系列/学习数据结构和算法的框架思维.md)提到过,链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现**这个问题具有递归性质**。 +首先,前文 [学习数据结构的框架思维](https://labuladong.online/algo/essential-technique/abstraction-of-algorithm/) 提到过,链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现**这个问题具有递归性质**。 什么叫递归性质?直接上图理解,比如说我们对这个链表调用 `reverseKGroup(head, 2)`,即以 2 个节点为一组反转链表: -![](../pictures/kgroup/1.jpg) +![](https://labuladong.online/algo/images/kgroup/1.jpg) 如果我设法把前 2 个节点反转,那么后面的那些节点怎么处理?后面的这些节点也是一条链表,而且规模(长度)比原来这条链表小,这就叫**子问题**。 -![](../pictures/kgroup/2.jpg) +![](https://labuladong.online/algo/images/kgroup/2.jpg) -我们可以直接递归调用 `reverseKGroup(cur, 2)`,因为子问题和原问题的结构完全相同,这就是所谓的递归性质。 +我们可以把原先的 `head` 指针移动到后面这一段链表的开头,然后继续递归调用 `reverseKGroup(head, 2)`,因为子问题(后面这部分链表)和原问题(整条链表)的结构完全相同,这就是所谓的递归性质。 发现了递归性质,就可以得到大致的算法流程: **1、先反转以 `head` 开头的 `k` 个元素**。 -![](../pictures/kgroup/3.jpg) +![](https://labuladong.online/algo/images/kgroup/3.jpg) **2、将第 `k + 1` 个元素作为 `head` 递归调用 `reverseKGroup` 函数**。 -![](../pictures/kgroup/4.jpg) +![](https://labuladong.online/algo/images/kgroup/4.jpg) **3、将上述两个过程的结果连接起来**。 -![](../pictures/kgroup/5.jpg) +![](https://labuladong.online/algo/images/kgroup/5.jpg) 整体思路就是这样了,最后一点值得注意的是,递归函数都有个 base case,对于这个问题是什么呢? @@ -46,6 +69,7 @@ 首先,我们要实现一个 `reverse` 函数反转一个区间之内的元素。在此之前我们再简化一下,给定链表头结点,如何反转整个链表? + ```java // 反转以 a 为头结点的链表 ListNode reverse(ListNode a) { @@ -64,7 +88,9 @@ ListNode reverse(ListNode a) { } ``` -![](../pictures/kgroup/8.gif) +算法执行的过程如下 GIF 所示:: + +![](https://labuladong.online/algo/images/kgroup/8.gif) 这次使用迭代思路来实现的,借助动画理解应该很容易。 @@ -72,6 +98,7 @@ ListNode reverse(ListNode a) { 只要更改函数签名,并把上面的代码中 `null` 改成 `b` 即可: + ```java /** 反转区间 [a, b) 的元素,注意是左闭右开 */ ListNode reverse(ListNode a, ListNode b) { @@ -91,6 +118,7 @@ ListNode reverse(ListNode a, ListNode b) { 现在我们迭代实现了反转部分链表的功能,接下来就按照之前的逻辑编写 `reverseKGroup` 函数即可: + ```java ListNode reverseKGroup(ListNode head, int k) { if (head == null) return null; @@ -112,27 +140,132 @@ ListNode reverseKGroup(ListNode head, int k) { 解释一下 `for` 循环之后的几句代码,注意 `reverse` 函数是反转区间 `[a, b)`,所以情形是这样的: -![](../pictures/kgroup/6.jpg) +![](https://labuladong.online/algo/images/kgroup/6.jpg) 递归部分就不展开了,整个函数递归完成之后就是这个结果,完全符合题意: -![](../pictures/kgroup/7.jpg) +![](https://labuladong.online/algo/images/kgroup/7.jpg) + + ### 三、最后说两句 从阅读量上看,基本数据结构相关的算法文章看的人都不多,我想说这是要吃亏的。 -大家喜欢看动态规划相关的问题,可能因为面试很常见,但就我个人理解,很多算法思想都是源于数据结构的。我们公众号的成名之作之一,「学习数据结构的框架思维」就提过,什么动规、回溯、分治算法,其实都是树的遍历,树这种结构它不就是个多叉链表吗?你能处理基本数据结构的问题,解决一般的算法问题应该也不会太费事。 +大家喜欢看动态规划相关的问题,可能因为面试很常见,但就我个人理解,很多算法思想都是源于数据结构的。我们公众号的成名之作之一,[学习数据结构的框架思维](https://labuladong.online/algo/essential-technique/abstraction-of-algorithm/) 就提过,什么动规、回溯、分治算法,其实都是树的遍历,树这种结构它不就是个多叉链表吗?你能处理基本数据结构的问题,解决一般的算法问题应该也不会太费事。 + +那么如何分解问题、发现递归性质呢?这个只能多练习,我在数据结构精品课中讲解了 [单链表的递归实现](https://aep.h5.xeknow.com/s/1RQzXc),应该能够让你进一步加深对递归的理解。 + + + +
+
+引用本文的文章 + + - [东哥带你刷二叉树(思路篇)](https://labuladong.online/algo/data-structure/binary-tree-part1/) + - [算法笔试「骗分」套路](https://labuladong.online/algo/other-skills/tips-in-exam/) + - [递归魔法:反转单链表](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | +| :----: | :----: | +| [24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/?show=1) | [24. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/?show=1) | + +
+
+ + + +**_____________** + +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: + +![](https://labuladong.online/algo/images/souyisou2.png) + +======其他语言代码====== + +[25.K个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group) + +### javascript -那么如何分解问题、发现递归性质呢?这个只能多练习,也许后续可以专门写一篇文章来探讨一下,本文就到此为止吧,希望对大家有帮助! +```js +/** + * Definition for singly-linked list. + * function ListNode(val, next) { + * this.val = (val===undefined ? 0 : val) + * this.next = (next===undefined ? null : next) + * } + */ -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +// 示例一:反转以a为头结点的链表 +let reverse = function (a) { + let pre, cur, nxt; + pre = null; + cur = a; + nxt = a; + while (cur != null) { + nxt = cur.next; + // 逐个结点反转 + cur.next = pre; + // 更新指针位置 + pre = cur; + cur = nxt; + } + // 返回反转后的头结点 + return pre; +} + +/** 反转区间 [a, b) 的元素,注意是左闭右开 */ +let reverse = (a, b) => { + let pre, cur, nxt; + pre = null; + cur = a; + nxt = a; + // while 终止的条件改一下就行了 + while (cur !== b) { + nxt = cur.next; + cur.next = pre; + pre = cur; + cur = nxt; + } + // 返回反转后的头结点 + return pre; +} -[上一篇:如何寻找最长回文子串](../高频面试系列/最长回文子串.md) +/** + * @param {ListNode} head + * @param {number} k + * @return {ListNode} + */ +let reverseKGroup = (head, k) => { + if (head == null) return null; + // 区间 [a, b) 包含 k 个待反转元素 + let a, b; + a = b = head; + for (let i = 0; i < k; i++) { + // 不足k个,不需反转,base case + if(b==null) return head; + b = b.next; + } + + // 反转前k个元素 + let newHead = reverse(a,b); -[下一篇:如何判定括号合法性](../高频面试系列/合法括号判定.md) + // 递归反转后续链表并连接起来 + a.next = reverseKGroup(b,k); + return newHead; +} +``` -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" index 7cd5853ec2..1e5bdc51ef 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" @@ -1,12 +1,37 @@ # 一行代码就能解决的算法题 -下文是我在 LeetCode 刷题过程中总结的三道有趣的「脑筋急转弯」题目,可以使用算法编程解决,但只要稍加思考,就能找到规律,直接想出答案。 +

+GitHub + + + +

+ +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [292. Nim Game](https://leetcode.com/problems/nim-game/) | [292. Nim 游戏](https://leetcode.cn/problems/nim-game/) | 🟢 +| [319. Bulb Switcher](https://leetcode.com/problems/bulb-switcher/) | [319. 灯泡开关](https://leetcode.cn/problems/bulb-switcher/) | 🟠 +| [877. Stone Game](https://leetcode.com/problems/stone-game/) | [877. 石子游戏](https://leetcode.cn/problems/stone-game/) | 🟠 + +**-----------** + +下文是我在刷题过程中总结的三道有趣的「脑筋急转弯」题目,可以使用算法编程解决,但只要稍加思考,就能找到规律,直接想出答案。 ### 一、Nim 游戏 -游戏规则是这样的:你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。 +力扣第 292 题「Nim 游戏」给了这样一个游戏规则: + +你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。 -假设你们都很聪明,由你第一个开始拿,请你写一个算法,输入一个正整数 n,返回你是否能赢(true 或 false)。 +假设你们都很聪明,由你第一个开始拿,请你写一个算法,输入一个正整数 `n`,返回你是否能赢(true 或 false)。 比如现在有 4 颗石子,算法应该返回 false。因为无论你拿 1 颗 2 颗还是 3 颗,对方都能一次性拿完,拿走最后一颗石子,所以你一定会输。 @@ -24,20 +49,22 @@ 这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单: -```cpp -bool canWinNim(int n) { + +```java +boolean canWinNim(int n) { // 如果上来就踩到 4 的倍数,那就认输吧 // 否则,可以把对方控制在 4 的倍数,必胜 return n % 4 != 0; } ``` - ### 二、石头游戏 -游戏规则是这样的:你和你的朋友面前有一排石头堆,用一个数组 piles 表示,piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。 +力扣第 877 题「石子游戏」的规则是这样的: + +你和你的朋友面前有一排石头堆,用一个数组 `piles` 表示,`piles[i]` 表示第 `i` 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。 -**假设你们都很聪明**,由你第一个开始拿,请你写一个算法,输入一个数组 piles,返回你是否能赢(true 或 false)。 +**假设你们都很聪明**,由你第一个开始拿,请你写一个算法,输入一个数组 `piles`,返回你是否能赢(true 或 false)。 注意,石头的堆的数量为偶数,所以你们两人拿走的堆数一定是相同的。石头的总数为奇数,也就是你们最后不可能拥有相同多的石头,一定有胜负之分。 @@ -49,7 +76,7 @@ bool canWinNim(int n) { 最后,你的对手只能拿 1 了。 -这样下来,你总共拥有 $2 + 9 = 11$ 颗石头,对手有 $5 + 1 = 6$ 颗石头,你是可以赢的,所以算法应该返回 true。 +这样下来,你总共拥有 `2 + 9 = 11` 颗石头,对手有 `5 + 1 = 6` 颗石头,你是可以赢的,所以算法应该返回 true。 你看到了,并不是简单的挑数字大的选,为什么第一次选择 2 而不是 5 呢?因为 5 后面是 9,你要是贪图一时的利益,就把 9 这堆石头暴露给对手了,那你就要输了。 @@ -57,6 +84,7 @@ bool canWinNim(int n) { 这道题又涉及到两人的博弈,也可以用动态规划算法暴力试,比较麻烦。但我们只要对规则深入思考,就会大惊失色:只要你足够聪明,你是必胜无疑的,因为你是先手。 + ```java boolean stoneGame(int[] piles) { return true; @@ -75,7 +103,9 @@ boolean stoneGame(int[] piles) { ### 三、电灯开关问题 -这个问题是这样描述的:有 n 盏电灯,最开始时都是关着的。现在要进行 n 轮操作: +力扣第 319 题「灯泡开关」的规则是这样的: + +有 `n` 盏电灯,最开始时都是关着的。现在要进行 `n` 轮操作: 第 1 轮操作是把每一盏电灯的开关按一下(全部打开)。 @@ -83,12 +113,13 @@ boolean stoneGame(int[] piles) { 第 3 轮操作是把每三盏灯的开关按一下(就是按第 3,6,9... 盏灯的开关,有的被关闭,比如 3,有的被打开,比如 6)... -如此往复,直到第 n 轮,即只按一下第 n 盏灯的开关。 +如此往复,直到第 `n` 轮,即只按一下第 `n` 盏灯的开关。 -现在给你输入一个正整数 n 代表电灯的个数,问你经过 n 轮操作后,这些电灯有多少盏是亮的? +现在给你输入一个正整数 `n` 代表电灯的个数,问你经过 `n` 轮操作后,这些电灯有多少盏是亮的? 我们当然可以用一个布尔数组表示这些灯的开关情况,然后模拟这些操作过程,最后去数一下就能出结果。但是这样显得没有灵性,最好的解法是这样的: + ```java int bulbSwitch(int n) { return (int)Math.sqrt(n); @@ -101,27 +132,148 @@ int bulbSwitch(int n) { 我们假设只有 6 盏灯,而且我们只看第 6 盏灯。需要进行 6 轮操作对吧,请问对于第 6 盏灯,会被按下几次开关呢?这不难得出,第 1 轮会被按,第 2 轮,第 3 轮,第 6 轮都会被按。 -为什么第 1、2、3、6 轮会被按呢?因为 $6=1\times6=2\times3$。一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次? +为什么第 1、2、3、6 轮会被按呢?因为 `6=1*6=2*3`。一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次? -$16=1\times16=2\times8=4\times4$ +`16 = 1*16 = 2*8 = 4*4` 其中因子 4 重复出现,所以第 16 盏灯会被按 5 次,奇数次。现在你应该理解这个问题为什么和平方根有关了吧? 不过,我们不是要算最后有几盏灯亮着吗,这样直接平方根一下是啥意思呢?稍微思考一下就能理解了。 -就假设现在总共有 16 盏灯,我们求 16 的平方根,等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 $1\times1=1$ 盏、第 $2\times2=4$ 盏、第 $3\times3=9$ 盏和第 $4\times4=16$ 盏。 +就假设现在总共有 16 盏灯,我们求 16 的平方根,等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 `1*1=1` 盏、第 `2*2=4` 盏、第 `3*3=9` 盏和第 `4*4=16` 盏。 + +就算有的 `n` 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。 + + -就算有的 n 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。 +
+
+引用本文的文章 + - [一文秒杀所有丑数系列问题](https://labuladong.online/algo/frequency-interview/ugly-number-summary/) + - [我的刷题心得:算法的本质](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [经典动态规划:博弈问题](https://labuladong.online/algo/dynamic-programming/game-theory/) +

-坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:Union-Find算法应用](../算法思维系列/UnionFind算法应用.md) -[下一篇:二分查找高效判定子序列](../高频面试系列/二分查找判定子序列.md) +**_____________** + +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: + +![](https://labuladong.online/algo/images/souyisou2.png) + +======其他语言代码====== + +[292.Nim游戏](https://leetcode-cn.com/problems/nim-game) + +[877.石子游戏](https://leetcode-cn.com/problems/stone-game) + +[319.灯泡开关](https://leetcode-cn.com/problems/bulb-switcher) + + + +### python + +由[JodyZ0203](https://github.com/JodyZ0203)提供 292. Nim 游戏 Python3 解法代码: + +```Python +class Solution: + def canWinNim(self, n: int) -> bool: + # 如果除于是0,说明是4的倍数,所以必输 + # 否则不是除于不等于0,说明不是4的倍数,说明必胜 + return n % 4 != 0 +``` + +由[JodyZ0203](https://github.com/JodyZ0203)提供 877. 石子游戏 Python3 解法代码: + +```Python +class Solution: + def stoneGame(self, piles: List[int]) -> bool: + # 双方都很聪明的前提下, 先手必胜无疑 + # 先手可以提前观察偶数堆还是基数的石头总数更多 + return True +``` + + +由[JodyZ0203](https://github.com/JodyZ0203)提供 319. 灯泡开关 Python3 解法代码: + +```Python +class Solution: + def bulbSwitch(self, n: int) -> int: + # 平方根电灯个数之后向下取整即可 + return floor(sqrt (n)) +``` + + + +### c++ + +由[JodyZ0203](https://github.com/JodyZ0203)提供 877. 石子游戏 C++ 解法代码: + +```cpp +class Solution { +public: + bool stoneGame(vector& piles) { + // 双方都很聪明的前提下, 先手必胜无疑 + return true; + } +}; +``` + + + + +由[JodyZ0203](https://github.com/JodyZ0203)提供 319. 灯泡开关 C++ 解法代码: + +```cpp +class Solution { +public: + int bulbSwitch(int n) { + // 平方根电灯个数之后向下取整即可 + return floor(sqrt (n)); + } +}; +``` + + + +### javascript + +[292.Nim游戏](https://leetcode-cn.com/problems/nim-game) + +```js +/** + * @param {number} n + * @return {boolean} + */ +var canWinNim = function(n) { + // 如果上来就踩到 4 的倍数,那就认输吧 + // 否则,可以把对方控制在 4 的倍数,必胜 + return n % 4 !== 0; +}; +``` + +[877.石子游戏](https://leetcode-cn.com/problems/stone-game) + +```js +var stoneGame = function(piles) { + return true; +}; +``` + +[319.灯泡开关](https://leetcode-cn.com/problems/bulb-switcher) + +```js +/** + * @param {number} n + * @return {number} + */ +var bulbSwitch = function(n) { + return Math.floor(Math.sqrt(n)); +}; +``` -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" index 4c18690380..6719573674 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" @@ -1,14 +1,42 @@ # 二分查找高效判定子序列 -二分查找本身不难理解,难在巧妙地运用二分查找技巧。对于一个问题,你可能都很难想到它跟二分查找有关,比如前文 [最长递增子序列](../动态规划系列/动态规划设计:最长递增子序列.md) 就借助一个纸牌游戏衍生出二分查找解法。 +

+GitHub + + + +

-今天再讲一道巧用二分查找的算法问题:如何判定字符串 `s` 是否是字符串 `t` 的子序列(可以假定 `s` 长度比较小,且 `t` 的长度非常大)。举两个例子: +![](https://labuladong.online/algo/images/souyisou1.png) +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** -s = "abc", t = "**a**h**b**gd**c**", return true. +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [392. Is Subsequence](https://leetcode.com/problems/is-subsequence/) | [392. 判断子序列](https://leetcode.cn/problems/is-subsequence/) | 🟢 +| [792. Number of Matching Subsequences](https://leetcode.com/problems/number-of-matching-subsequences/) | [792. 匹配子序列的单词数](https://leetcode.cn/problems/number-of-matching-subsequences/) | 🟠 + +**-----------** + +二分查找本身不难理解,难在巧妙地运用二分查找技巧。 + +对于一个问题,你可能都很难想到它跟二分查找有关,比如前文 [最长递增子序列](https://labuladong.online/algo/dynamic-programming/longest-increasing-subsequence/) 就借助一个纸牌游戏衍生出二分查找解法。 + +今天再讲一道巧用二分查找的算法问题,力扣第 392 题「判断子序列」: + +请你判定字符串 `s` 是否是字符串 `t` 的子序列(可以假定 `s` 长度比较小,且 `t` 的长度非常大)。 + +举两个例子: + +``` +s = "abc", t = "**a**h**b**gd**c**", return true. + s = "axc", t = "ahbgdc", return false. +``` 题目很容易理解,而且看起来很简单,但很难想到这个问题跟二分查找有关吧? @@ -16,20 +44,25 @@ s = "axc", t = "ahbgdc", return false. 首先,一个很简单的解法是这样的: -```cpp -bool isSubsequence(string s, string t) { + +```java +boolean isSubsequence(String s, String t) { int i = 0, j = 0; - while (i < s.size() && j < t.size()) { - if (s[i] == t[j]) i++; + while (i < s.length() && j < t.length()) { + if (s.charAt(i) == t.charAt(j)) { + i++; + } j++; } - return i == s.size(); + return i == s.length(); } ``` + + 其思路也非常简单,利用双指针 `i, j` 分别指向 `s, t`,一边前进一边匹配子序列: -![gif](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/1.gif) +![](https://labuladong.online/algo/images/子序列/1.gif) 读者也许会问,这不就是最优解法了吗,时间复杂度只需 O(N),N 为 `t` 的长度。 @@ -37,6 +70,7 @@ bool isSubsequence(string s, string t) { 如果给你一系列字符串 `s1,s2,...` 和字符串 `t`,你需要判定每个串 `s` 是否是 `t` 的子序列(可以假定 `s` 较短,`t` 很长)。 + ```java boolean[] isSubsequence(String[] sn, String t); ``` @@ -49,6 +83,7 @@ boolean[] isSubsequence(String[] sn, String t); 二分思路主要是对 `t` 进行预处理,用一个字典 `index` 将每个字符出现的索引位置按顺序存储下来: + ```java int m = s.length(), n = t.length(); ArrayList[] index = new ArrayList[256]; @@ -61,48 +96,51 @@ for (int i = 0; i < n; i++) { } ``` -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/2.jpg) +![](https://labuladong.online/algo/images/子序列/2.jpg) 比如对于这个情况,匹配了 "ab",应该匹配 "c" 了: -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/1.jpg) +![](https://labuladong.online/algo/images/子序列/1.jpg) 按照之前的解法,我们需要 `j` 线性前进扫描字符 "c",但借助 `index` 中记录的信息,**可以二分搜索 `index[c]` 中比 j 大的那个索引**,在上图的例子中,就是在 `[0,2,6]` 中搜索比 4 大的那个索引: -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/3.jpg) +![](https://labuladong.online/algo/images/子序列/3.jpg) 这样就可以直接得到下一个 "c" 的索引。现在的问题就是,如何用二分查找计算那个恰好比 4 大的索引呢?答案是,寻找左侧边界的二分搜索就可以做到。 ### 三、再谈二分查找 -在前文 [二分查找详解](../算法思维系列/二分查找详解.md) 中,详解了如何正确写出三种二分查找算法的细节。二分查找返回目标值 `val` 的索引,对于搜索**左侧边界**的二分查找,有一个特殊性质: +在前文 [二分查找详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) 中,详解了如何正确写出三种二分查找算法的细节。二分查找返回目标值 `val` 的索引,对于搜索**左侧边界**的二分查找,有一个特殊性质: **当 `val` 不存在时,得到的索引恰好是比 `val` 大的最小元素索引**。 什么意思呢,就是说如果在数组 `[0,1,3,4]` 中搜索元素 2,算法会返回索引 2,也就是元素 3 的位置,元素 3 是数组中大于 2 的最小元素。所以我们可以利用二分搜索避免线性扫描。 + ```java // 查找左侧边界的二分查找 -int left_bound(ArrayList arr, int tar) { - int lo = 0, hi = arr.size(); - while (lo < hi) { - int mid = lo + (hi - lo) / 2; - if (tar > arr.get(mid)) { - lo = mid + 1; +int left_bound(ArrayList arr, int target) { + int left = 0, right = arr.size(); + while (left < right) { + int mid = left + (right - left) / 2; + if (target > arr.get(mid)) { + left = mid + 1; } else { - hi = mid; + right = mid; } } - return lo; + if (left == arr.size()) { + return -1; + } + return left; } ``` -以上就是搜索左侧边界的二分查找,等会儿会用到,其中的细节可以参见前文《二分查找详解》,这里不再赘述。 - -### 四、代码实现 +以上就是搜索左侧边界的二分查找,等会儿会用到,其中的细节可以参见前文 [二分查找详解](https://labuladong.online/algo/essential-technique/binary-search-framework/),这里不再赘述。 这里以单个字符串 `s` 为例,对于多个字符串 `s`,可以把预处理部分抽出来。 + ```java boolean isSubsequence(String s, String t) { int m = s.length(), n = t.length(); @@ -124,7 +162,7 @@ boolean isSubsequence(String s, String t) { if (index[c] == null) return false; int pos = left_bound(index[c], j); // 二分搜索区间中没有找到字符 c - if (pos == index[c].size()) return false; + if (pos == -1) return false; // 向前移动指针 j j = index[c].get(pos) + 1; } @@ -134,17 +172,220 @@ boolean isSubsequence(String s, String t) { 算法执行的过程是这样的: -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/2.gif) +![](https://labuladong.online/algo/images/子序列/2.gif) + +可见借助二分查找,算法的效率是可以大幅提升的。 + +明白了这个思路,我们可以直接拿下力扣第 792 题「匹配子序列的单词数」:给你输入一个字符串列表 `words` 和一个字符串 `s`,问你 `words` 中有多少字符串是 `s` 的子序列。 + +函数签名如下: + + +```java +int numMatchingSubseq(String s, String[] words) +``` + +我们直接把上一道题的代码稍微改改即可完成这道题: + + +```java +int numMatchingSubseq(String s, String[] words) { + // 对 s 进行预处理 + // char -> 该 char 的索引列表 + ArrayList[] index = new ArrayList[256]; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (index[c] == null) { + index[c] = new ArrayList<>(); + } + index[c].add(i); + } + + int res = 0; + for (String word : words) { + // 字符串 word 上的指针 + int i = 0; + // 串 s 上的指针 + int j = 0; + // 借助 index 查找 word 中每个字符的索引 + for (; i < word.length(); i++) { + char c = word.charAt(i); + // 整个 s 压根儿没有字符 c + if (index[c] == null) { + break; + } + int pos = left_bound(index[c], j); + // 二分搜索区间中没有找到字符 c + if (pos == -1) { + break; + } + // 向前移动指针 j + j = index[c].get(pos) + 1; + } + // 如果 word 完成匹配,则是子序列 + if (i == word.length()) { + res++; + } + } + + return res; +} + +// 查找左侧边界的二分查找 +int left_bound(ArrayList arr, int target) { + // 见上文 +} +``` + + + -可见借助二分查找,算法的效率是可以大幅提升的。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: +**_____________** -![labuladong](../pictures/labuladong.jpg) +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: +![](https://labuladong.online/algo/images/souyisou2.png) -[上一篇:一行代码就能解决的算法题](../高频面试系列/一行代码解决的智力题.md) +======其他语言代码====== -[下一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) +[392.判断子序列](https://leetcode-cn.com/problems/is-subsequence) + +### c++ + +[dekunma](https://www.linkedin.com/in/dekun-ma-036a9b198/) 提供C++代码 +**解法一:遍历(也可以用双指针):** + +```C++ +class Solution { +public: + bool isSubsequence(string s, string t) { + // 遍历s + for(int i = 0; i < s.size(); i++) { + // 找到s[i]字符在t中的位置 + size_t pos = t.find(s[i]); + + // 如果s[i]字符不在t中,返回false + if(pos == std::string::npos) return false; + // 如果s[i]在t中,后面就只看pos以后的字串,防止重复查找 + else t = t.substr(pos + 1); + } + return true; + } +}; +``` + +**解法二:二分查找:** +```C++ +class Solution { +public: + bool isSubsequence(string s, string t) { + int m = s.size(), n = t.size(); + // 对 t 进行预处理 + vector index[256]; + for (int i = 0; i < n; i++) { + char c = t[i]; + index[c].push_back(i); + } + // 串 t 上的指针 + int j = 0; + // 借助 index 查找 s[i] + for (int i = 0; i < m; i++) { + char c = s[i]; + // 整个 t 压根儿没有字符 c + if (index[c].empty()) return false; + int pos = left_bound(index[c], j); + // 二分搜索区间中没有找到字符 c + if (pos == index[c].size()) return false; + // 向前移动指针 j + j = index[c][pos] + 1; + } + return true; + } + // 查找左侧边界的二分查找 + int left_bound(vector arr, int tar) { + int lo = 0, hi = arr.size(); + while (lo < hi) { + int mid = lo + (hi - lo) / 2; + if (tar > arr[mid]) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; + } +}; +``` + + + +### javascript + +双指针一遍扫描做法 + +```js +/** + * @param {string} s + * @param {string} t + * @return {boolean} + */ +var isSubsequence = function (s, t) { + let i = 0, j = 0; + while (i < s.length && j < t.length) { + if (s[i] === t[j]) i++; + j++; + } + return i === s.length; +}; +``` + + + +**升级:二分法做法,可应对与多个s的情况** + +```js +var isSubsequence = function (s, t) { + let m = s.length, n = t.length; + let index = new Array(256); + // 先记下 t 中每个字符出现的位置 + for (let i = 0; i < n; i++) { + let c = t[i]; + if (index[c] == null) { + index[c] = []; + } + index[c].push(i) + } + + // 串t上的指针 + let j = 0; + // 借助index查找s[i] + for (let i = 0; i < m; i++) { + let c = s[i]; + // 整个t压根没有字符c + if (index[c] == null) return false + let pos = left_bound(index[c], j); + // 二分搜索区间中没有找到字符c + if (pos == index[c].length) return false; + + // 向前移动指针j + j = index[c][pos] + 1; + } + return true; +}; + +var left_bound = function (arr, tar) { + let lo = 0, hi = arr.length; + while (lo < hi) { + + let mid = lo + Math.floor((hi - lo) / 2); + if (tar > arr[mid]) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; +} +``` -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" new file mode 100644 index 0000000000..e10782e306 --- /dev/null +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" @@ -0,0 +1,676 @@ +# 实际运用二分搜索时的思维框架 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1011. Capacity To Ship Packages Within D Days](https://leetcode.com/problems/capacity-to-ship-packages-within-d-days/) | [1011. 在 D 天内送达包裹的能力](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/) | 🟠 | +| [410. Split Array Largest Sum](https://leetcode.com/problems/split-array-largest-sum/) | [410. 分割数组的最大值](https://leetcode.cn/problems/split-array-largest-sum/) | 🔴 | +| [875. Koko Eating Bananas](https://leetcode.com/problems/koko-eating-bananas/) | [875. 爱吃香蕉的珂珂](https://leetcode.cn/problems/koko-eating-bananas/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二分查找框架详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) + +在 [二分查找框架详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) 中我们详细研究了二分搜索的细节问题,探讨了「搜索一个元素」,「搜索左侧边界」,「搜索右侧边界」这三个情况,教你如何写出正确无 bug 的二分搜索算法。 + +**但是前文总结的二分搜索代码框架仅仅局限于「在有序数组中搜索指定元素」这个基本场景,具体的算法问题没有这么直接,可能你都很难看出这个问题能够用到二分搜索**。 + +所以本文就来总结一套二分搜索算法运用的框架套路,帮你在遇到二分搜索算法相关的实际问题时,能够有条理地思考分析,步步为营,写出答案。 + + + + + + + +## 原始的二分搜索代码 + +二分搜索的原型就是在「**有序数组**」中搜索一个元素 `target`,返回该元素对应的索引。 + +如果该元素不存在,那可以返回一个什么特殊值,这种细节问题只要微调算法实现就可实现。 + +还有一个重要的问题,如果「**有序数组**」中存在多个 `target` 元素,那么这些元素肯定挨在一起,这里就涉及到算法应该返回最左侧的那个 `target` 元素的索引还是最右侧的那个 `target` 元素的索引,也就是所谓的「搜索左侧边界」和「搜索右侧边界」,这个也可以通过微调算法的代码来实现。 + +**我们前文 [二分搜索核心框架](https://labuladong.online/algo/essential-technique/binary-search-framework/) 详细探讨了上述问题,对这块还不清楚的读者建议复习前文**,已经搞清楚基本二分搜索算法的读者可以继续看下去。 + +**在具体的算法问题中,常用到的是「搜索左侧边界」和「搜索右侧边界」这两种场景**,很少有让你单独「搜索一个元素」。 + +因为算法题一般都让你求最值,比如让你求吃香蕉的「最小速度」,让你求轮船的「最低运载能力」,求最值的过程,必然是搜索一个边界的过程,所以后面我们就详细分析一下这两种搜索边界的二分算法代码。 + +> [!NOTE] +> 注意,本文我写的都是左闭右开的二分搜索写法,如果你习惯两端都闭的写法,可以自行改写代码。 + +「搜索左侧边界」的二分搜索算法的具体代码实现如下: + +```java +// 搜索左侧边界 +int left_bound(int[] nums, int target) { + if (nums.length == 0) return -1; + int left = 0, right = nums.length; + + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + // 当找到 target 时,收缩右侧边界 + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left; +} +``` + +假设输入的数组 `nums = [1,2,3,3,3,5,7]`,想搜索的元素 `target = 3`,那么算法就会返回索引 2。 + +如果画一个图,就是这样: + +![](https://labuladong.online/algo/images/binary-search-in-action/1.jpeg) + +「搜索右侧边界」的二分搜索算法的具体代码实现如下: + +```java +// 搜索右侧边界 +int right_bound(int[] nums, int target) { + if (nums.length == 0) return -1; + int left = 0, right = nums.length; + + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + // 当找到 target 时,收缩左侧边界 + left = mid + 1; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left - 1; +} +``` + +输入同上,那么算法就会返回索引 4,如果画一个图,就是这样: + +![](https://labuladong.online/algo/images/binary-search-in-action/2.jpeg) + +好,上述内容都属于复习,我想读到这里的读者应该都能理解。记住上述的图像,所有能够抽象出上述图像的问题,都可以使用二分搜索解决。 + + + + + + + +## 二分搜索问题的泛化 + +什么问题可以运用二分搜索算法技巧? + +**首先,你要从题目中抽象出一个自变量 `x`,一个关于 `x` 的函数 `f(x)`,以及一个目标值 `target`**。 + +同时,`x, f(x), target` 还要满足以下条件: + +**1、`f(x)` 必须是在 `x` 上的单调函数(单调增单调减都可以)**。 + +**2、题目是让你计算满足约束条件 `f(x) == target` 时的 `x` 的值**。 + +上述规则听起来有点抽象,来举个具体的例子: + +给你一个升序排列的有序数组 `nums` 以及一个目标元素 `target`,请你计算 `target` 在数组中的索引位置,如果有多个目标元素,返回最小的索引。 + +这就是「搜索左侧边界」这个基本题型,解法代码之前都写了,但这里面 `x, f(x), target` 分别是什么呢? + +我们可以把数组中元素的索引认为是自变量 `x`,函数关系 `f(x)` 就可以这样设定: + +```java +// 函数 f(x) 是关于自变量 x 的单调递增函数 +// 入参 nums 是不会改变的,所以可以忽略,不算自变量 +int f(int x, int[] nums) { + return nums[x]; +} +``` + +其实这个函数 `f` 就是在访问数组 `nums`,因为题目给我们的数组 `nums` 是升序排列的,所以函数 `f(x)` 就是在 `x` 上单调递增的函数。 + +最后,题目让我们求什么来着?是不是让我们计算元素 `target` 的最左侧索引? + +是不是就相当于在问我们「满足 `f(x) == target` 的 `x` 的最小值是多少」? + +画个图,如下: + +![](https://labuladong.online/algo/images/binary-search-in-action/3.jpeg) + +**如果遇到一个算法问题,能够把它抽象成这幅图,就可以对它运用二分搜索算法**。 + +算法代码如下: + +```java +// 函数 f 是关于自变量 x 的单调递增函数 +int f(int x, int[] nums) { + return nums[x]; +} + +int left_bound(int[] nums, int target) { + if (nums.length == 0) return -1; + int left = 0, right = nums.length; + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(mid, nums) == target) { + // 当找到 target 时,收缩右侧边界 + right = mid; + } else if (f(mid, nums) < target) { + left = mid + 1; + } else if (f(mid, nums) > target) { + right = mid; + } + } + return left; +} +``` + +这段代码其实是多此一举,把之常规的二分搜索代码微调了一下,把直接访问 `nums[mid]` 套了一层函数 `f`。但是,这样能抽象出二分搜索思想在具体算法问题中的框架。 + +## 运用二分搜索的套路框架 + +想要运用二分搜索解决具体的算法问题,可以从以下代码框架着手思考: + +```java +// 函数 f 是关于自变量 x 的单调函数 +int f(int x) { + // ... +} + +// 主函数,在 f(x) == target 的约束下求 x 的最值 +int solution(int[] nums, int target) { + if (nums.length == 0) return -1; + // 问自己:自变量 x 的最小值是多少? + int left = ...; + // 问自己:自变量 x 的最大值是多少? + int right = ... + 1; + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(mid) == target) { + // 问自己:题目是求左边界还是右边界? + // ... + } else if (f(mid) < target) { + // 问自己:怎么让 f(x) 大一点? + // ... + } else if (f(mid) > target) { + // 问自己:怎么让 f(x) 小一点? + // ... + } + } + return left; +} +``` + +具体来说,想要用二分搜索算法解决问题,分为以下几步: + +**1、确定 `x, f(x), target` 分别是什么,并写出函数 `f` 的代码**。 + +**2、找到 `x` 的取值范围作为二分搜索的搜索区间,初始化 `left` 和 `right` 变量**。 + +**3、根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码**。 + +下面用几道例题来讲解这个流程。 + +## 例题一、珂珂吃香蕉 + +这是力扣第 875 题「爱吃香蕉的珂珂」: + + + +珂珂每小时最多只能吃一堆香蕉,如果吃不完的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。 + +他想在警卫回来之前吃完所有香蕉,让我们确定吃香蕉的**最小速度 `K`**。函数签名如下: + +```java +int minEatingSpeed(int[] piles, int H); +``` + +那么,对于这道题,如何运用刚才总结的套路,写出二分搜索解法代码? + +按步骤思考即可: + +**1、确定 `x, f(x), target` 分别是什么,并写出函数 `f` 的代码**。 + +自变量 `x` 是什么呢?回忆之前的函数图像,二分搜索的本质就是在搜索自变量。 + +所以,题目让求什么,就把什么设为自变量,珂珂吃香蕉的速度就是自变量 `x`。 + +那么,在 `x` 上单调的函数关系 `f(x)` 是什么? + +显然,吃香蕉的速度越快,吃完所有香蕉堆所需的时间就越少,速度和时间就是一个单调函数关系。 + +所以,`f(x)` 函数就可以这样定义: + +若吃香蕉的速度为 `x` 根/小时,则需要 `f(x)` 小时吃完所有香蕉。 + +代码实现如下: + +```java +// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉 +// f(x) 随着 x 的增加单调递减 +long f(int[] piles, int x) { + long hours = 0; + for (int i = 0; i < piles.length; i++) { + hours += piles[i] / x; + if (piles[i] % x > 0) { + hours++; + } + } + return hours; +} +``` + +> [!NOTE] +> 为什么 `f(x)` 的返回值是 `long` 类型?因为你注意题目给的数据范围和 `f` 函数的逻辑。`piles` 数组中元素的最大值是 10^9,最多有 10^4 个元素;那么当 `x` 取值为 1 时,`hours` 变量就会被加到 10^13 这个数量级,超过了 `int` 类型的最大值(大概 2x10^9 这个量级),所以这里用 `long` 类型避免可能出现的整型溢出。 + +`target` 就很明显了,吃香蕉的时间限制 `H` 自然就是 `target`,是对 `f(x)` 返回值的最大约束。 + +**2、找到 `x` 的取值范围作为二分搜索的搜索区间,初始化 `left` 和 `right` 变量**。 + +珂珂吃香蕉的速度最小是多少?多大是多少? + +显然,最小速度应该是 1,最大速度是 `piles` 数组中元素的最大值,因为每小时最多吃一堆香蕉,胃口再大也白搭嘛。 + +这里可以有两种选择,要么你用一个 for 循环去遍历 `piles` 数组,计算最大值,要么你看题目给的约束,`piles` 中的元素取值范围是多少,然后给 `right` 初始化一个取值范围之外的值。 + +我选择第二种,题目说了 `1 <= piles[i] <= 10^9`,那么我就可以确定二分搜索的区间边界: + +```java +public int minEatingSpeed(int[] piles, int H) { + int left = 1; + // 注意,我选择左闭右开的二分搜索写法,right 是开区间,所以再加一 + int right = 1000000000 + 1; + + // ... +} +``` + +因为我们二分搜索是对数级别的复杂度,所以 `right` 就算是个很大的值,算法的效率依然很高。 + +**3、根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码**。 + +现在我们确定了自变量 `x` 是吃香蕉的速度,`f(x)` 是单调递减的函数,`target` 就是吃香蕉的时间限制 `H`,题目要我们计算最小速度,也就是 `x` 要尽可能小: + +![](https://labuladong.online/algo/images/binary-search-in-action/4.jpeg) + +这就是搜索左侧边界的二分搜索嘛,不过注意 `f(x)` 是单调递减的,不要闭眼睛套框架,需要结合上图进行思考,写出代码: + +```java +class Solution { + public int minEatingSpeed(int[] piles, int H) { + int left = 1; + int right = 1000000000 + 1; + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(piles, mid) == H) { + // 搜索左侧边界,则需要收缩右侧边界 + right = mid; + } else if (f(piles, mid) < H) { + // 需要让 f(x) 的返回值大一些 + right = mid; + } else if (f(piles, mid) > H) { + // 需要让 f(x) 的返回值小一些 + left = mid + 1; + } + } + return left; + } +} +``` + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +> [!TIP] +> 我这里采用的是左闭右开的二分搜索写法,如果想用两端都闭的写法,只要修改 `right` 的初始值和 `right` 更新的逻辑即可: +> +> +> +> +> +> ```java +> // 两端都闭的二分搜索写法 +> int minEatingSpeed(int[] piles, int H) { +> int left = 1; +> // right 是闭区间,所以这里改成最大取值 +> int right = 1000000000; +> +> // right 是闭区间,所以这里改成 <= +> while (left <= right) { +> int mid = left + (right - left) / 2; +> if (f(piles, mid) <= H) { +> // right 是闭区间,所以这里用 mid - 1 +> right = mid - 1; +> } else { +> left = mid + 1; +> } +> } +> return left; +> } +> ``` +> +> +> +> 关于这个算法中的细节问题,前文 [二分搜索算法详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) 进行了详细分析,这里不展开了。 + +至此,这道题就解决了。我们代码框架中多余的 if 分支主要是帮助理解的,写出正确解法后建议合并多余的分支,可以提高算法运行的效率: + +```java +class Solution { + public int minEatingSpeed(int[] piles, int H) { + int left = 1; + int right = 1000000000 + 1; + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(piles, mid) <= H) { + right = mid; + } else { + left = mid + 1; + } + } + return left; + } + + // f(x) 随着 x 的增加单调递减 + long f(int[] piles, int x) { + long hours = 0; + for (int i = 0; i < piles.length; i++) { + hours += piles[i] / x; + if (piles[i] % x > 0) { + hours++; + } + } + return hours; + } +} +``` + +## 例题二、运送货物 + +再看看力扣第 1011 题「在 D 天内送达包裹的能力」: + + + +要在 `D` 天内按顺序运输完所有货物,货物不可分割,如何确定运输的最小载重呢? + +函数签名如下: + +```java +int shipWithinDays(int[] weights, int days); +``` + +和上一道题一样的,我们按照流程来就行: + +**1、确定 `x, f(x), target` 分别是什么,并写出函数 `f` 的代码**。 + +题目问什么,什么就是自变量,也就是说船的运载能力就是自变量 `x`。 + +运输天数和运载能力成反比,所以可以让 `f(x)` 计算 `x` 的运载能力下需要的运输天数,那么 `f(x)` 是单调递减的。 + +函数 `f(x)` 的实现如下: + +```java +// 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物 +// f(x) 随着 x 的增加单调递减 +int f(int[] weights, int x) { + int days = 0; + for (int i = 0; i < weights.length; ) { + // 尽可能多装货物 + int cap = x; + while (i < weights.length) { + if (cap < weights[i]) break; + else cap -= weights[i]; + i++; + } + days++; + } + return days; +} +``` + +对于这道题,`target` 显然就是运输天数 `D`,我们要在 `f(x) == D` 的约束下,算出船的最小载重。 + +**2、找到 `x` 的取值范围作为二分搜索的搜索区间,初始化 `left` 和 `right` 变量**。 + +船的最小载重是多少?最大载重是多少? + +显然,船的最小载重应该是 `weights` 数组中元素的最大值,因为每次至少得装一件货物走,不能说装不下嘛。 + +最大载重显然就是`weights` 数组所有元素之和,也就是一次把所有货物都装走。 + +这样就确定了搜索区间 `[left, right)`: + +```java +int shipWithinDays(int[] weights, int days) { + int left = 0; + // 注意,right 是开区间,所以额外加一 + int right = 1; + for (int w : weights) { + left = Math.max(left, w); + right += w; + } + + // ... +} +``` + +**3、需要根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码**。 + +现在我们确定了自变量 `x` 是船的载重能力,`f(x)` 是单调递减的函数,`target` 就是运输总天数限制 `D`,题目要我们计算船的最小载重,也就是 `x` 要尽可能小: + +![](https://labuladong.online/algo/images/binary-search-in-action/5.jpeg) + +这就是搜索左侧边界的二分搜索嘛,结合上图就可写出二分搜索代码: + +```java +public int shipWithinDays(int[] weights, int days) { + int left = 0; + // 注意,right 是开区间,所以额外加一 + int right = 1; + for (int w : weights) { + left = Math.max(left, w); + right += w; + } + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(weights, mid) == days) { + // 搜索左侧边界,则需要收缩右侧边界 + right = mid; + } else if (f(weights, mid) < days) { + // 需要让 f(x) 的返回值大一些 + right = mid; + } else if (f(weights, mid) > days) { + // 需要让 f(x) 的返回值小一些 + left = mid + 1; + } + } + + return left; +} +``` + +到这里,这道题的解法也写出来了,我们合并一下多余的 if 分支,提高代码运行速度,最终代码如下: + +```java +class Solution { + public int shipWithinDays(int[] weights, int days) { + int left = 0; + int right = 1; + for (int w : weights) { + left = Math.max(left, w); + right += w; + } + + while (left < right) { + int mid = left + (right - left) / 2; + if (f(weights, mid) <= days) { + right = mid; + } else { + left = mid + 1; + } + } + + return left; + } + + int f(int[] weights, int x) { + int days = 0; + for (int i = 0; i < weights.length; ) { + int cap = x; + while (i < weights.length) { + if (cap < weights[i]) break; + else cap -= weights[i]; + i++; + } + days++; + } + return days; + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +## 例题三、分割数组 + +我们实操一下力扣第 410 题「分割数组的最大值」,难度为困难: + + + +函数签名如下: + +```java +int splitArray(int[] nums, int m); +``` + +这个题目有点类似前文一道经典动态规划题目 [高楼扔鸡蛋](https://labuladong.online/algo/dynamic-programming/egg-drop/),题目比较绕,又是最大值又是最小值的。 + +简单说,给你输入一个数组 `nums` 和数字 `m`,你要把 `nums` 分割成 `m` 个子数组。 + +肯定有不止一种分割方法,每种分割方法都会把 `nums` 分成 `m` 个子数组,这 `m` 个子数组中肯定有一个和最大的子数组对吧。 + +我们想要找一个分割方法,该方法分割出的最大子数组和是所有方法中最大子数组和最小的。 + +请你的算法返回这个分割方法对应的最大子数组和。 + +我滴妈呀,这个题目看了就觉得难的不行,完全没思路,这题怎么运用我们之前说套路,转化成二分搜索呢? + +**其实,这道题和上面讲的运输问题是一模一样的,不相信的话我给你改写一下题目**: + +你只有一艘货船,现在有若干货物,每个货物的重量是 `nums[i]`,现在你需要在 `m` 天内将这些货物运走,请问你的货船的最小载重是多少? + +这不就是刚才我们解决的力扣第 1011 题「在 D 天内送达包裹的能力」吗? + +货船每天运走的货物就是 `nums` 的一个子数组;在 `m` 天内运完就是将 `nums` 划分成 `m` 个子数组;让货船的载重尽可能小,就是让所有子数组中最大的那个子数组元素之和尽可能小。 + +所以这道题的解法直接复制粘贴运输问题的解法代码即可: + +```java +class Solution { + public int splitArray(int[] nums, int m) { + return shipWithinDays(nums, m); + } + + int shipWithinDays(int[] weights, int days) { + // 见上文 + } + + int f(int[] weights, int x) { + // 见上文 + } +} +``` + +本文就到这里,总结来说,如果发现题目中存在单调关系,就可以尝试使用二分搜索的思路来解决。搞清楚单调性和二分搜索的种类,通过分析和画图,就能够写出最终的代码。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】二分搜索算法经典习题](https://labuladong.online/algo/problem-set/binary-search/) + - [【强化练习】回溯算法经典习题 II](https://labuladong.online/algo/problem-set/backtrack-ii/) + - [一文秒杀所有丑数系列问题](https://labuladong.online/algo/frequency-interview/ugly-number-summary/) + - [二分搜索算法核心代码模板](https://labuladong.online/algo/essential-technique/binary-search-framework/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [用算法打败算法](https://labuladong.online/algo/fname.html?fname=PDF中的算法) + - [经典动态规划:高楼扔鸡蛋](https://labuladong.online/algo/dynamic-programming/egg-drop/) + - [讲两道常考的阶乘算法题](https://labuladong.online/algo/frequency-interview/factorial-problems/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1201. Ugly Number III](https://leetcode.com/problems/ugly-number-iii/?show=1) | [1201. 丑数 III](https://leetcode.cn/problems/ugly-number-iii/?show=1) | 🟠 | +| [1723. Find Minimum Time to Finish All Jobs](https://leetcode.com/problems/find-minimum-time-to-finish-all-jobs/?show=1) | [1723. 完成所有工作的最短时间](https://leetcode.cn/problems/find-minimum-time-to-finish-all-jobs/?show=1) | 🔴 | +| - | [剑指 Offer II 073. 狒狒吃香蕉](https://leetcode.cn/problems/nZZqjQ/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" index 5a280b0d5d..d5c39b7a2e 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" @@ -1,31 +1,63 @@ -我们之前有两篇文章写了回文串和回文序列相关的问题。 +# 如何判断回文链表 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [234. Palindrome Linked List](https://leetcode.com/problems/palindrome-linked-list/) | [234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) | 🟢 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [链表基础](https://labuladong.online/algo/data-structure-basic/linkedlist-basic/) +> - [链表双指针技巧](https://labuladong.online/algo/essential-technique/linked-list-skills-summary/) +> - [数组双指针技巧汇总](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) + +前文 [数组双指针技巧汇总](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) 讲解了回文串和回文序列相关的问题,先来简单回顾下。 **寻找**回文串的核心思想是从中心向两端扩展: -```cpp -string palindrome(string& s, int l, int r) { +```java +// 在 s 中寻找以 s[left] 和 s[right] 为中心的最长回文串 +String palindrome(String s, int left, int right) { // 防止索引越界 - while (l >= 0 && r < s.size() - && s[l] == s[r]) { - // 向两边展开 - l--; r++; + while (left >= 0 && right < s.length() + && s.charAt(left) == s.charAt(right)) { + // 双指针,向两边展开 + left--; + right++; } - // 返回以 s[l] 和 s[r] 为中心的最长回文串 - return s.substr(l + 1, r - l - 1); + // 返回以 s[left] 和 s[right] 为中心的最长回文串 + return s.substring(left + 1, right); } ``` -因为回文串长度可能为奇数也可能是偶数,长度为奇数时只存在一个中心点,而长度为偶数时存在两个中心点,所以上面这个函数需要传入`l`和`r`。 +因为回文串长度可能为奇数也可能是偶数,长度为奇数时只存在一个中心点,而长度为偶数时存在两个中心点,所以上面这个函数需要传入 `l` 和 `r`。 -而**判断**一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要「双指针技巧」,从两端向中间逼近即可: +而**判断**一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要[双指针技巧](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/),从两端向中间逼近即可: -```cpp -bool isPalindrome(string s) { - int left = 0, right = s.length - 1; +```java +boolean isPalindrome(String s) { + // 一左一右两个指针相向而行 + int left = 0, right = s.length() - 1; while (left < right) { - if (s[left] != s[right]) + if (s.charAt(left) != s.charAt(right)) { return false; - left++; right--; + } + left++; + right--; } return true; } @@ -35,35 +67,26 @@ bool isPalindrome(string s) { 下面扩展这一最简单的情况,来解决:如何判断一个「单链表」是不是回文。 -### 一、判断回文单链表 - -输入一个单链表的头结点,判断这个链表中的数字是不是回文: +## 一、判断回文单链表 -```java -/** - * 单链表节点的定义: - * public class ListNode { - * int val; - * ListNode next; - * } - */ +看下力扣第 234 题「回文链表」: -boolean isPalindrome(ListNode head); + -输入: 1->2->null -输出: false +函数签名如下: -输入: 1->2->2->1->null -输出: true +```java +boolean isPalindrome(ListNode head); ``` -这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。关于如何反转链表,可以参见前文「递归操作链表」。 +这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。 -其实,**借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表**,下面来具体聊聊。 +那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。关于如何反转链表,可以参见前文 [递归翻转链表的一部分](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/)。 -对于二叉树的几种遍历方式,我们再熟悉不过了: +我在 [学习数据结构的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) 中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,**链表其实也可以有前序遍历和后序遍历,借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表**: ```java +// 二叉树遍历框架 void traverse(TreeNode root) { // 前序遍历代码 traverse(root.left); @@ -71,11 +94,8 @@ void traverse(TreeNode root) { traverse(root.right); // 后序遍历代码 } -``` -在「学习数据结构的框架思维」中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,**链表其实也可以有前序遍历和后序遍历**: - -```java +// 递归遍历单链表 void traverse(ListNode head) { // 前序遍历代码 traverse(head.next); @@ -83,10 +103,10 @@ void traverse(ListNode head) { } ``` -这个框架有什么指导意义呢?如果我想正序打印链表中的`val`值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作: +这个框架有什么指导意义呢?如果我想正序打印链表中的 `val` 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作: ```java -/* 倒序打印单链表中的元素值 */ +// 倒序打印单链表中的元素值 void traverse(ListNode head) { if (head == null) return; traverse(head.next); @@ -98,35 +118,54 @@ void traverse(ListNode head) { 说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能: ```java -// 左侧指针 -ListNode left; +class Solution { + // 从左向右移动的指针 + ListNode left; + // 从右向左移动的指针 + ListNode right; + + // 记录链表是否为回文 + boolean res = true; + + boolean isPalindrome(ListNode head) { + left = head; + traverse(head); + return res; + } -boolean isPalindrome(ListNode head) { - left = head; - return traverse(head); -} + void traverse(ListNode right) { + if (right == null) { + return; + } -boolean traverse(ListNode right) { - if (right == null) return true; - boolean res = traverse(right.next); - // 后序遍历代码 - res = res && (right.val == left.val); - left = left.next; - return res; + // 利用递归,走到链表尾部 + traverse(right.next); + + // 后序遍历位置,此时的 right 指针指向链表右侧尾部 + // 所以可以和 left 指针比较,判断是否是回文链表 + if (left.val != right.val) { + res = false; + } + left = left.next; + } } ``` 这么做的核心逻辑是什么呢?**实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的**,只不过我们利用的是递归函数的堆栈而已。 -![](../pictures/回文链表/1.gif) + + +你可以点开下面这个可视化面板,多次点击 if (right === null) 这一行代码,即可看到 `right` 指针利用递归堆栈走到了链表尾部,然后再多次点击 left = left.next; 这一行代码,即可看到 `left` 前进,`right` 指针回退,相向而行,最终完成回文判断: + + -当然,无论造一条反转链表还是利用后续遍历,算法的时间和空间复杂度都是 O(N)。下面我们想想,能不能不用额外的空间,解决这个问题呢? +当然,无论造一条反转链表还是利用后序遍历,算法的时间和空间复杂度都是 O(N)。下面我们想想,能不能不用额外的空间,解决这个问题呢? -### 二、优化空间复杂度 +## 二、优化空间复杂度 更好的思路是这样的: -**1、先通过「双指针技巧」中的快慢指针来找到链表的中点**: +**1、先通过 [链表双指针技巧](https://labuladong.online/algo/essential-technique/linked-list-skills-summary/) 中的快慢指针来找到链表的中点**: ```java ListNode slow, fast; @@ -138,7 +177,9 @@ while (fast != null && fast.next != null) { // slow 指针现在指向链表中点 ``` -![](../pictures/回文链表/1.jpg) +![](https://labuladong.online/algo/images/palindrome-list/1.jpg) + + **2、如果`fast`指针没有指向`null`,说明链表长度为奇数,`slow`还要再前进一步**: @@ -147,7 +188,7 @@ if (fast != null) slow = slow.next; ``` -![](../pictures/回文链表/2.jpg) +![](https://labuladong.online/algo/images/palindrome-list/2.jpg) **3、从`slow`开始反转后面的链表,现在就可以开始比较回文串了**: @@ -164,24 +205,59 @@ while (right != null) { return true; ``` -![](../pictures/回文链表/3.jpg) +![](https://labuladong.online/algo/images/palindrome-list/3.jpg) + -至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中`reverse`函数很容易实现: + +至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中 `reverse` 函数可以参考 [翻转单链表](https://labuladong.online/algo/data-structure/reverse-linked-list-recursion/): ```java -ListNode reverse(ListNode head) { - ListNode pre = null, cur = head; - while (cur != null) { - ListNode next = cur.next; - cur.next = pre; - pre = cur; - cur = next; +class Solution { + public boolean isPalindrome(ListNode head) { + ListNode slow, fast; + slow = fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + + if (fast != null) + slow = slow.next; + + ListNode left = head; + ListNode right = reverse(slow); + while (right != null) { + if (left.val != right.val) + return false; + left = left.next; + right = right.next; + } + + return true; + } + + ListNode reverse(ListNode head) { + ListNode pre = null, cur = head; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; } - return pre; } ``` -![](../pictures/kgroup/8.gif) +算法过程如下 GIF 所示: + +![](https://labuladong.online/algo/images/kgroup/8.gif) + + + +你可以点开下面的可视化面板,多次点击 while (right != null) 这一行代码,即可看到 `left` 和 `right` 指针相向而行,最终完成回文判断: + + 算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。 @@ -189,7 +265,7 @@ ListNode reverse(ListNode head) { 其实这个问题很好解决,关键在于得到`p, q`这两个指针位置: -![](../pictures/回文链表/4.jpg) +![](https://labuladong.online/algo/images/palindrome-list/4.jpg) 这样,只要在函数 return 之前加一段代码即可恢复原先链表顺序: @@ -199,19 +275,38 @@ p.next = reverse(q); 篇幅所限,我就不写了,读者可以自己尝试一下。 -### 三、最后总结 +## 三、最后总结 首先,寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。对于单链表,无法直接倒序遍历,可以造一条新的反转链表,可以利用链表的后序遍历,也可以用栈结构倒序处理单链表。 具体到回文链表的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到 O(1)。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:如何寻找缺失和重复的元素](../高频面试系列/缺失和重复的元素.md) -[下一篇:如何在无限序列中随机抽取元素](../高频面试系列/水塘抽样.md) -[目录](../README.md#目录) \ No newline at end of file + + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| - | [剑指 Offer II 027. 回文链表](https://leetcode.cn/problems/aMhZSa/?show=1) | 🟢 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" deleted file mode 100644 index 3e3cdceb63..0000000000 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" +++ /dev/null @@ -1,93 +0,0 @@ -# 如何判定括号合法性 - -对括号的合法性判断是一个很常见且实用的问题,比如说我们写的代码,编辑器和编译器都会检查括号是否正确闭合。而且我们的代码可能会包含三种括号 `[](){}`,判断起来有一点难度。 - -本文就来聊一道关于括号合法性判断的算法题,相信能加深你对**栈**这种数据结构的理解。 - -题目很简单,输入一个字符串,其中包含 `[](){}` 六种括号,请你判断这个字符串组成的括号是否合法。 - -``` -Input: "()[]{}" -Output: true - -Input: "([)]" -Output: false - -Input: "{[]}" -Output: true -``` - -解决这个问题之前,我们先降低难度,思考一下,**如果只有一种括号 `()`**,应该如何判断字符串组成的括号是否合法呢? - -### 一、处理一种括号 - -字符串中只有圆括号,如果想让括号字符串合法,那么必须做到: - -**每个右括号 `)` 的左边必须有一个左括号 `(` 和它匹配**。 - -比如说字符串 `()))((` 中,中间的两个右括号**左边**就没有左括号匹配,所以这个括号组合是不合法的。 - -那么根据这个思路,我们可以写出算法: - -```cpp -bool isValid(string str) { - // 待匹配的左括号数量 - int left = 0; - for (char c : str) { - if (c == '(') - left++; - else // 遇到右括号 - left--; - - if (left < 0) - return false; - } - return left == 0; -} -``` -如果只有圆括号,这样就能正确判断合法性。对于三种括号的情况,我一开始想模仿这个思路,定义三个变量 `left1`,`left2`,`left3` 分别处理每种括号,虽然要多写不少 if else 分支,但是似乎可以解决问题。 - -但实际上直接照搬这种思路是不行的,比如说只有一个括号的情况下 `(())` 是合法的,但是多种括号的情况下, `[(])` 显然是不合法的。 - -仅仅记录每种左括号出现的次数已经不能做出正确判断了,我们要加大存储的信息量,可以利用栈来模仿类似的思路。 - -### 二、处理多种括号 - -栈是一种先进后出的数据结构,处理括号问题的时候尤其有用。 - -我们这道题就用一个名为 `left` 的栈代替之前思路中的 `left` 变量,**遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配**。 - -```cpp -bool isValid(string str) { - stack left; - for (char c : str) { - if (c == '(' || c == '{' || c == '[') - left.push(c); - else // 字符 c 是右括号 - if (!left.empty() && leftOf(c) == left.top()) - left.pop(); - else - // 和最近的左括号不匹配 - return false; - } - // 是否所有的左括号都被匹配了 - return left.empty(); -} - -char leftOf(char c) { - if (c == '}') return '{'; - if (c == ')') return '('; - return '['; -} -``` - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:如何k个一组反转链表](../高频面试系列/k个一组反转链表.md) - -[下一篇:如何寻找消失的元素](../高频面试系列/消失的元素.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..80d2b72474 --- /dev/null +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" @@ -0,0 +1,282 @@ +# 众里寻他千百度:名流问题 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [277. Find the Celebrity](https://leetcode.com/problems/find-the-celebrity/)🔒 | [277. 搜寻名人](https://leetcode.cn/problems/find-the-celebrity/)🔒 | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [图结构基础及通用实现](https://labuladong.online/algo/data-structure-basic/graph-basic/) + +今天来讨论经典的「名流问题」: + +给你 `n` 个人的社交关系(你知道任意两个人之间是否认识),然后请你找出这些人中的「名人」。 + +所谓「名人」有两个条件: + +1、所有其他人都认识「名人」。 + +2、「名人」不认识任何其他人。 + +这是一个图相关的算法问题,社交关系嘛,本质上就可以抽象成一幅图。 + +如果把每个人看做图中的节点,「认识」这种关系看做是节点之间的有向边,那么名人就是这幅图中一个特殊的节点: + +![](https://labuladong.online/algo/images/celebrity/1.jpeg) + +**这个节点没有一条指向其他节点的有向边;且其他所有节点都有一条指向这个节点的有向边**。 + +或者说的专业一点,名人节点的出度为 0,入度为 `n - 1`。 + +那么,这 `n` 个人的社交关系是如何表示的呢? + +前文 [图论算法基础](https://labuladong.online/algo/data-structure-basic/graph-basic/) 说过,图有两种存储形式,一种是邻接表,一种是邻接矩阵,邻接表的主要优势是节约存储空间;邻接矩阵的主要优势是可以迅速判断两个节点是否相邻。 + +对于名人问题,显然会经常需要判断两个人之间是否认识,也就是两个节点是否相邻,所以我们可以用邻接矩阵来表示人和人之间的社交关系。 + +那么,把名流问题描述成算法的形式就是这样的: + +给你输入一个大小为 `n x n` 的二维数组(邻接矩阵) `graph` 表示一幅有 `n` 个节点的图,每个人都是图中的一个节点,编号为 `0` 到 `n - 1`。 + +如果 `graph[i][j] == 1` 代表第 `i` 个人认识第 `j` 个人,如果 `graph[i][j] == 0` 代表第 `i` 个人不认识第 `j` 个人。 + +有了这幅图表示人与人之间的关系,请你计算,这 `n` 个人中,是否存在「名人」? + +如果存在,算法返回这个名人的编号,如果不存在,算法返回 -1。 + +函数签名如下: + +```java +int findCelebrity(int[][] graph); +``` + +比如输入的邻接矩阵长这样: + +![](https://labuladong.online/algo/images/celebrity/2.jpeg) + +那么算法应该返回 2。 + +力扣第 277 题「搜寻名人」就是这个经典问题,不过并不是直接把邻接矩阵传给你,而是只告诉你总人数 `n`,同时提供一个 API `knows` 来查询人和人之间的社交关系: + +```java +// 可以直接调用,能够返回 i 是否认识 j +boolean knows(int i, int j); + +// 请你实现:返回「名人」的编号 +int findCelebrity(int n) { + // todo +} +``` + +很明显,`knows` API 本质上还是在访问邻接矩阵。为了简单起见,我们后面就按力扣的题目形式来探讨一下这个经典问题。 + +## 暴力解法 + +我们拍拍脑袋就能写出一个简单粗暴的算法: + +```java +class Solution extends Relation { + public int findCelebrity(int n) { + for (int cand = 0; cand < n; cand++) { + int other; + for (other = 0; other < n; other++) { + if (cand == other) continue; + // 保证其他人都认识 cand,且 cand 不认识任何其他人 + // 否则 cand 就不可能是名人 + if (knows(cand, other) || !knows(other, cand)) { + break; + } + } + if (other == n) { + // 找到名人 + return cand; + } + } + // 没有一个人符合名人特性 + return -1; + } +} +``` + +`cand` 是候选人(candidate)的缩写,我们的暴力算法就是从头开始穷举,把每个人都视为候选人,判断是否符合「名人」的条件。 + +刚才也说了,`knows` 函数底层就是在访问一个二维的邻接矩阵,一次调用的时间复杂度是 O(1),所以这个暴力解法整体的最坏时间复杂度是 O(N^2)。 + +那么,是否有其他高明的办法来优化时间复杂度呢?其实是有优化空间的,你想想,我们现在最耗时的地方在哪里? + +对于每一个候选人 `cand`,我们都要用一个内层 for 循环去判断这个 `cand` 到底符不符合「名人」的条件。 + +这个内层 for 循环看起来就蠢,虽然判断一个人「是名人」必须用一个 for 循环,但判断一个人「不是名人」就不用这么麻烦了。 + +**因为「名人」的定义保证了「名人」的唯一性,所以我们可以利用排除法,先排除那些显然不是「名人」的人,从而避免 for 循环的嵌套,降低时间复杂度**。 + + + + + + + +## 优化解法 + +我再重复一遍所谓「名人」的定义: + +1、所有其他人都认识名人。 + +2、名人不认识任何其他人。 + +这个定义就很有意思,它保证了人群中最多有一个名人。 + +这很好理解,如果有两个人同时是名人,那么这两条定义就自相矛盾了。 + +**换句话说,只要观察任意两个候选人的关系,我一定能确定其中的一个人不是名人,把他排除**。 + +至于另一个候选人是不是名人,只看两个人的关系肯定是不能确定的,但这不重要,重要的是排除掉一个必然不是名人的候选人,缩小了包围圈。 + +这是优化的核心,也是比较难理解的,所以我们先来说说为什么观察任意两个候选人的关系,就能排除掉一个。 + +你想想,两个人之间的关系可能是什么样的? + +无非就是四种:你认识我我不认识你,我认识你你不认识我,咱俩互相认识,咱两互相不认识。 + +如果把人比作节点,红色的有向边表示不认识,绿色的有向边表示认识,那么两个人的关系无非是如下四种情况: + +![](https://labuladong.online/algo/images/celebrity/3.jpeg) + +不妨认为这两个人的编号分别是 `cand` 和 `other`,然后我们逐一分析每种情况,看看怎么排除掉一个人。 + +对于情况一,`cand` 认识 `other`,所以 `cand` 肯定不是名人,排除。因为名人不可能认识别人。 + +对于情况二,`other` 认识 `cand`,所以 `other` 肯定不是名人,排除。 + +对于情况三,他俩互相认识,肯定都不是名人,可以随便排除一个。 + +对于情况四,他俩互不认识,肯定都不是名人,可以随便排除一个。因为名人应该被所有其他人认识。 + +综上,只要观察任意两个之间的关系,就至少能确定一个人不是名人,上述情况判断可以用如下代码表示: + +```java +if (knows(cand, other) || !knows(other, cand)) { + // cand 不可能是名人 +} else { + // other 不可能是名人 +} +``` + +如果能够理解这一个特点,那么写出优化解法就简单了。 + +**我们可以不断从候选人中选两个出来,然后排除掉一个,直到最后只剩下一个候选人,这时候再使用一个 for 循环判断这个候选人是否是货真价实的「名人」**。 + +这个思路的完整代码如下: + +```java +class Solution extends Relation { + public int findCelebrity(int n) { + if (n == 1) return 0; + // 将所有候选人装进队列 + LinkedList q = new LinkedList<>(); + for (int i = 0; i < n; i++) { + q.addLast(i); + } + // 一直排除,直到只剩下一个候选人停止循环 + while (q.size() >= 2) { + // 每次取出两个候选人,排除一个 + int cand = q.removeFirst(); + int other = q.removeFirst(); + if (knows(cand, other) || !knows(other, cand)) { + // cand 不可能是名人,排除,让 other 归队 + q.addFirst(other); + } else { + // other 不可能是名人,排除,让 cand 归队 + q.addFirst(cand); + } + } + + // 现在排除得只剩一个候选人,判断他是否真的是名人 + int cand = q.removeFirst(); + for (int other = 0; other < n; other++) { + if (other == cand) { + continue; + } + // 保证其他人都认识 cand,且 cand 不认识任何其他人 + if (!knows(other, cand) || knows(cand, other)) { + return -1; + } + } + // cand 是名人 + return cand; + } +} +``` + +这个算法避免了嵌套 for 循环,时间复杂度降为 O(N) 了,不过引入了一个队列来存储候选人集合,使用了 O(N) 的空间复杂度。 + +> [!NOTE] +> `LinkedList` 的作用只是充当一个容器把候选人装起来,每次找出两个进行比较和淘汰,但至于具体找出哪两个,都是无所谓的,也就是说候选人归队的顺序无所谓,我们用的是 `addFirst` 只是方便后续的优化,你完全可以用 `addLast`,结果都是一样的。 + +是否可以进一步优化,把空间复杂度也优化掉? + +## 最终解法 + +如果你能够理解上面的优化解法,其实可以不需要额外的空间解决这个问题,代码如下: + +```java +class Solution extends Relation { + public int findCelebrity(int n) { + int cand = 0; + for (int other = 1; other < n; other++) { + if (!knows(other, cand) || knows(cand, other)) { + // cand 不可能是名人,排除 + // 假设 other 是名人 + cand = other; + } else { + // other 不可能是名人,排除 + // 什么都不用做,继续假设 cand 是名人 + } + } + + // 现在的 cand 是排除的最后结果,但不能保证一定是名人 + for (int other = 0; other < n; other++) { + if (cand == other) continue; + // 需要保证其他人都认识 cand,且 cand 不认识任何其他人 + if (!knows(other, cand) || knows(cand, other)) { + return -1; + } + } + + return cand; + } +} +``` + +我们之前的解法用到了 `LinkedList` 充当一个队列,用于存储候选人集合,而这个优化解法利用 `other` 和 `cand` 的交替变化,模拟了我们之前操作队列的过程,避免了使用额外的存储空间。 + +现在,解决名人问题的解法时间复杂度为 O(N),空间复杂度为 O(1),已经是最优解法了。 + + + + + + + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" deleted file mode 100644 index 0ed97d889a..0000000000 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" +++ /dev/null @@ -1,73 +0,0 @@ -# 如何去除有序数组的重复元素 - -我们知道对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。 - -所以对于一般处理数组的算法问题,我们要尽可能只对数组尾部的元素进行操作,以避免额外的时间复杂度。 - -这篇文章讲讲如何对一个有序数组去重,先看下题目: - -![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/title.png) - -显然,由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难,但如果毎找到一个重复元素就立即删除它,就是在数组中间进行删除操作,整个时间复杂度是会达到 O(N^2)。而且题目要求我们原地修改,也就是说不能用辅助数组,空间复杂度得是 O(1)。 - -其实,**对于数组相关的算法问题,有一个通用的技巧:要尽量避免在中间删除元素,那我就想先办法把这个元素换到最后去**。这样的话,最终待删除的元素都拖在数组尾部,一个一个 pop 掉就行了,每次操作的时间复杂度也就降低到 O(1) 了。 - -按照这个思路呢,又可以衍生出解决类似需求的通用方式:双指针技巧。具体一点说,应该是快慢指针。 - -我们让慢指针 `slow` 走左后面,快指针 `fast` 走在前面探路,找到一个不重复的元素就告诉 `slow` 并让 `slow` 前进一步。这样当 `fast` 指针遍历完整个数组 `nums` 后,**`nums[0..slow]` 就是不重复元素,之后的所有元素都是重复元素**。 - -```java -int removeDuplicates(int[] nums) { - int n = nums.length; - if (n == 0) return 0; - int slow = 0, fast = 1; - while (fast < n) { - if (nums[fast] != nums[slow]) { - slow++; - // 维护 nums[0..slow] 无重复 - nums[slow] = nums[fast]; - } - fast++; - } - // 长度为索引 + 1 - return slow + 1; -} -``` - -看下算法执行的过程: - -![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/1.gif) - -再简单扩展一下,如果给你一个有序链表,如何去重呢?其实和数组是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已: - -```java -ListNode deleteDuplicates(ListNode head) { - if (head == null) return null; - ListNode slow = head, fast = head.next; - while (fast != null) { - if (fast.val != slow.val) { - // nums[slow] = nums[fast]; - slow.next = fast; - // slow++; - slow = slow.next; - } - // fast++ - fast = fast.next; - } - // 断开与后面重复元素的连接 - slow.next = null; - return head; -} -``` - -![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/2.gif) - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) - -[上一篇:如何高效解决接雨水问题](../高频面试系列/接雨水.md) - -[下一篇:如何寻找最长回文子串](../高频面试系列/最长回文子串.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" index 15b0274285..b71e0d0eb8 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" @@ -1,258 +1,1139 @@ -今天就来聊三道考察频率高,而且容易让人搞混的算法问题,分别是求子集(subset),求排列(permutation),求组合(combination)。 +# 回溯算法秒杀所有排列/组合/子集问题 -这几个问题都可以用回溯算法模板解决,同时子集问题还可以用数学归纳思想解决。读者可以记住这几个问题的回溯套路,就不怕搞不清了。 -### 一、子集 -问题很简单,输入一个**不包含重复数字**的数组,要求算法输出这些数字的所有子集。 +![](https://labuladong.online/algo/images/souyisou1.png) -```cpp -vector> subsets(vector& nums); +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [216. Combination Sum III](https://leetcode.com/problems/combination-sum-iii/) | [216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/) | 🟠 | +| [39. Combination Sum](https://leetcode.com/problems/combination-sum/) | [39. 组合总和](https://leetcode.cn/problems/combination-sum/) | 🟠 | +| [40. Combination Sum II](https://leetcode.com/problems/combination-sum-ii/) | [40. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) | 🟠 | +| [46. Permutations](https://leetcode.com/problems/permutations/) | [46. 全排列](https://leetcode.cn/problems/permutations/) | 🟠 | +| [47. Permutations II](https://leetcode.com/problems/permutations-ii/) | [47. 全排列 II](https://leetcode.cn/problems/permutations-ii/) | 🟠 | +| [77. Combinations](https://leetcode.com/problems/combinations/) | [77. 组合](https://leetcode.cn/problems/combinations/) | 🟠 | +| [78. Subsets](https://leetcode.com/problems/subsets/) | [78. 子集](https://leetcode.cn/problems/subsets/) | 🟠 | +| [90. Subsets II](https://leetcode.com/problems/subsets-ii/) | [90. 子集 II](https://leetcode.cn/problems/subsets-ii/) | 🟠 | +| - | [剑指 Offer II 082. 含有重复元素集合的组合](https://leetcode.cn/problems/4sjJUc/) | 🟠 | +| - | [剑指 Offer II 084. 含有重复元素集合的全排列](https://leetcode.cn/problems/7p8L0Z/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树系列算法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) +> - [回溯算法核心框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) + +> tip:本文有视频版:[回溯算法秒杀所有排列/组合/子集问题](https://www.bilibili.com/video/BV1Yt4y1t7dK/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。 + + + +虽然排列、组合、子集系列问题是高中就学过的,但如果想编写算法解决它们,还是非常考验计算机思维的,本文就讲讲编程解决这几个问题的核心思路,以后再有什么变体,你也能手到擒来,以不变应万变。 + +无论是排列、组合还是子集问题,简单说无非就是让你从序列 `nums` 中以给定规则取若干元素,主要有以下几种变体: + +**形式一、元素无重不可复选,即 `nums` 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式**。 + +以组合为例,如果输入 `nums = [2,3,6,7]`,和为 7 的组合应该只有 `[7]`。 + +**形式二、元素可重不可复选,即 `nums` 中的元素可以存在重复,每个元素最多只能被使用一次**。 + +以组合为例,如果输入 `nums = [2,5,2,1,2]`,和为 7 的组合应该有两种 `[2,2,2,1]` 和 `[5,2]`。 + +**形式三、元素无重可复选,即 `nums` 中的元素都是唯一的,每个元素可以被使用若干次**。 + +以组合为例,如果输入 `nums = [2,3,6,7]`,和为 7 的组合应该有两种 `[2,2,3]` 和 `[7]`。 + +当然,也可以说有第四种形式,即元素可重可复选。但既然元素可复选,那又何必存在重复元素呢?元素去重之后就等同于形式三,所以这种情况不用考虑。 + +上面用组合问题举的例子,但排列、组合、子集问题都可以有这三种基本形式,所以共有 9 种变化。 + + + + + + + +除此之外,题目也可以再添加各种限制条件,比如让你求和为 `target` 且元素个数为 `k` 的组合,那这么一来又可以衍生出一堆变体,怪不得面试笔试中经常考到排列组合这种基本题型。 + +**但无论形式怎么变化,其本质就是穷举所有解,而这些解呈现树形结构,所以合理使用回溯算法框架,稍改代码框架即可把这些问题一网打尽**。 + +具体来说,你需要先阅读并理解前文 [回溯算法核心套路](https://labuladong.online/algo/essential-technique/backtrack-framework/),然后记住如下子集问题和排列问题的回溯树,就可以解决所有排列组合子集相关的问题: + +![](https://labuladong.online/algo/images/permutation/1.jpeg) + +![](https://labuladong.online/algo/images/permutation/2.jpeg) + +为什么只要记住这两种树形结构就能解决所有相关问题呢? + +**首先,组合问题和子集问题其实是等价的,这个后面会讲;至于之前说的三种变化形式,无非是在这两棵树上剪掉或者增加一些树枝罢了**。 + +那么,接下来我们就开始穷举,把排列/组合/子集问题的 9 种形式都过一遍,学学如何用回溯算法把它们一套带走。 + +> [!NOTE] +> 另外,有些读者之前看过的排列/子集/组合的解法代码可能和我在本文介绍的代码不同。这是因为回溯算法有两种穷举视角,我会在后文 [球盒模型:回溯算法穷举的两种视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) 手把手给你讲清楚。现在还不适合直接跟你讲那些解法,你照着我的思路学习即可。 + + + + + + + +## 子集(元素无重不可复选) + +力扣第 78 题「子集」就是这个问题: + +题目给你输入一个无重复元素的数组 `nums`,其中每个元素最多使用一次,请你返回 `nums` 的所有子集。 + +函数签名如下: + +```java +List> subsets(int[] nums) +``` + +比如输入 `nums = [1,2,3]`,算法应该返回如下子集: + +```java +[ [],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3] ] ``` -比如输入 `nums = [1,2,3]`,你的算法应输出 8 个子集,包含空集和本身,顺序可以不同: +好,我们暂时不考虑如何用代码实现,先回忆一下我们的高中知识,如何手推所有子集? + +首先,生成元素个数为 0 的子集,即空集 `[]`,为了方便表示,我称之为 `S_0`。 + +然后,在 `S_0` 的基础上生成元素个数为 1 的所有子集,我称为 `S_1`: + +![](https://labuladong.online/algo/images/permutation/3.jpeg) + +接下来,我们可以在 `S_1` 的基础上推导出 `S_2`,即元素个数为 2 的所有子集: -[ [],[1],[2],[3],[1,3],[2,3],[1,2],[1,2,3] ] +![](https://labuladong.online/algo/images/permutation/4.jpeg) -**第一个解法是利用数学归纳的思想**:假设我现在知道了规模更小的子问题的结果,如何推导出当前问题的结果呢? +为什么集合 `[2]` 只需要添加 `3`,而不添加前面的 `1` 呢? -具体来说就是,现在让你求 `[1,2,3]` 的子集,如果你知道了 `[1,2]` 的子集,是否可以推导出 `[1,2,3]` 的子集呢?先把 `[1,2]` 的子集写出来瞅瞅: +因为集合中的元素不用考虑顺序,`[1,2,3]` 中 `2` 后面只有 `3`,如果你添加了前面的 `1`,那么 `[2,1]` 会和之前已经生成的子集 `[1,2]` 重复。 -[ [],[1],[2],[1,2] ] +**换句话说,我们通过保证元素之间的相对顺序不变来防止出现重复的子集**。 -你会发现这样一个规律: +接着,我们可以通过 `S_2` 推出 `S_3`,实际上 `S_3` 中只有一个集合 `[1,2,3]`,它是通过 `[1,2]` 推出的。 -subset(`[1,2,3]`) - subset(`[1,2]`) +整个推导过程就是这样一棵树: -= [3],[1,3],[2,3],[1,2,3] +![](https://labuladong.online/algo/images/permutation/5.jpeg) -而这个结果,就是把 sebset(`[1,2]`) 的结果中每个集合再添加上 3。 +注意这棵树的特性: -换句话说,如果 `A = subset([1,2])` ,那么: +**如果把根节点作为第 0 层,将每个节点和根节点之间树枝上的元素作为该节点的值,那么第 `n` 层的所有节点就是大小为 `n` 的所有子集**。 -subset(`[1,2,3]`) +你比如大小为 2 的子集就是这一层节点的值: -= A + [A[i].add(3) for i = 1..len(A)] +![](https://labuladong.online/algo/images/permutation/6.jpeg) -这就是一个典型的递归结构嘛,`[1,2,3]` 的子集可以由 `[1,2]` 追加得出,`[1,2]` 的子集可以由 `[1]` 追加得出,base case 显然就是当输入集合为空集时,输出子集也就是一个空集。 +> [!NOTE] +> **注意,本文之后所说「节点的值」都是指节点和根节点之间树枝上的元素,且将根节点认为是第 0 层**。 -翻译成代码就很容易理解了: +那么再进一步,如果想计算所有子集,那只要遍历这棵多叉树,把所有节点的值收集起来不就行了? -```cpp -vector> subsets(vector& nums) { - // base case,返回一个空集 - if (nums.empty()) return {{}}; - // 把最后一个元素拿出来 - int n = nums.back(); - nums.pop_back(); - // 先递归算出前面元素的所有子集 - vector> res = subsets(nums); +直接看代码: + +```java +class Solution { + + List> res = new LinkedList<>(); + // 记录回溯算法的递归路径 + LinkedList track = new LinkedList<>(); + + // 主函数 + public List> subsets(int[] nums) { + backtrack(nums, 0); + return res; + } - int size = res.size(); - for (int i = 0; i < size; i++) { - // 然后在之前的结果之上追加 - res.push_back(res[i]); - res.back().push_back(n); + // 回溯算法核心函数,遍历子集问题的回溯树 + void backtrack(int[] nums, int start) { + + // 前序位置,每个节点的值都是一个子集 + res.add(new LinkedList<>(track)); + + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 做选择 + track.addLast(nums[i]); + // 通过 start 参数控制树枝的遍历,避免产生重复的子集 + backtrack(nums, i + 1); + // 撤销选择 + track.removeLast(); + } } - return res; } ``` -**这个问题的时间复杂度计算比较容易坑人**。我们之前说的计算递归算法时间复杂度的方法,是找到递归深度,然后乘以每次递归中迭代的次数。对于这个问题,递归深度显然是 N,但我们发现每次递归 for 循环的迭代次数取决于 `res` 的长度,并不是固定的。 +看过前文 [回溯算法核心框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) 的读者应该很容易理解这段代码吧,我们使用 `start` 参数控制树枝的生长避免产生重复的子集,用 `track` 记录根节点到每个节点的路径的值,同时在前序位置把每个节点的路径值收集起来,完成回溯树的遍历就收集了所有子集: -根据刚才的思路,`res` 的长度应该是每次递归都翻倍,所以说总的迭代次数应该是 2^N。或者不用这么麻烦,你想想一个大小为 N 的集合的子集总共有几个?2^N 个对吧,所以说至少要对 `res` 添加 2^N 次元素。 +![](https://labuladong.online/algo/images/permutation/5.jpeg) -那么算法的时间复杂度就是 O(2^N) 吗?还是不对,2^N 个子集是 `push_back` 添加进 `res` 的,所以要考虑 `push_back` 这个操作的效率: +最后,`backtrack` 函数开头看似没有 base case,会不会进入无限递归? -```cpp -for (int i = 0; i < size; i++) { - res.push_back(res[i]); // O(N) - res.back().push_back(n); // O(1) -} -``` +其实不会的,当 `start == nums.length` 时,叶子节点的值会被装入 `res`,但 for 循环不会执行,也就结束了递归。 + + + + + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +## 组合(元素无重不可复选) + +如果你能够成功的生成所有无重子集,那么你稍微改改代码就能生成所有无重组合了。 -因为 `res[i]` 也是一个数组呀,`push_back` 是把 `res[i]` copy 一份然后添加到数组的最后,所以一次操作的时间是 O(N)。 +你比如说,让你在 `nums = [1,2,3]` 中拿 2 个元素形成所有的组合,你怎么做? -综上,总的时间复杂度就是 O(N*2^N),还是比较耗时的。 +稍微想想就会发现,大小为 2 的所有组合,不就是所有大小为 2 的子集嘛。 -空间复杂度的话,如果不计算储存返回结果所用的空间的,只需要 O(N) 的递归堆栈空间。如果计算 `res` 所需的空间,应该是 O(N*2^N)。 +**所以我说组合和子集是一样的:大小为 `k` 的组合就是大小为 `k` 的子集**。 -**第二种通用方法就是回溯算法**。旧文「回溯算法详解」写过回溯算法的模板: +比如力扣第 77 题「组合」: -```python -result = [] -def backtrack(路径, 选择列表): - if 满足结束条件: - result.add(路径) - return - for 选择 in 选择列表: - 做选择 - backtrack(路径, 选择列表) - 撤销选择 +给定两个整数 `n` 和 `k`,返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。 + +函数签名如下: + +```java +List> combine(int n, int k) ``` -只要改造回溯算法的模板就行了: +比如 `combine(3, 2)` 的返回值应该是: -```cpp -vector> res; +```java +[ [1,2],[1,3],[2,3] ] +``` -vector> subsets(vector& nums) { - // 记录走过的路径 - vector track; - backtrack(nums, 0, track); - return res; -} +这是标准的组合问题,但我给你翻译一下就变成子集问题了: -void backtrack(vector& nums, int start, vector& track) { - res.push_back(track); - for (int i = start; i < nums.size(); i++) { - // 做选择 - track.push_back(nums[i]); - // 回溯 - backtrack(nums, i + 1, track); - // 撤销选择 - track.pop_back(); +**给你输入一个数组 `nums = [1,2..,n]` 和一个正整数 `k`,请你生成所有大小为 `k` 的子集**。 + +还是以 `nums = [1,2,3]` 为例,刚才让你求所有子集,就是把所有节点的值都收集起来;**现在你只需要把第 2 层(根节点视为第 0 层)的节点收集起来,就是大小为 2 的所有组合**: + +![](https://labuladong.online/algo/images/permutation/6.jpeg) + +反映到代码上,只需要稍改 base case,控制算法仅仅收集第 `k` 层节点的值即可: + +```java +class Solution { + + List> res = new LinkedList<>(); + // 记录回溯算法的递归路径 + LinkedList track = new LinkedList<>(); + + // 主函数 + public List> combine(int n, int k) { + backtrack(1, n, k); + return res; + } + + void backtrack(int start, int n, int k) { + // base case + if (k == track.size()) { + // 遍历到了第 k 层,收集当前节点的值 + res.add(new LinkedList<>(track)); + return; + } + + // 回溯算法标准框架 + for (int i = start; i <= n; i++) { + // 选择 + track.addLast(i); + // 通过 start 参数控制树枝的遍历,避免产生重复的子集 + backtrack(i + 1, n, k); + // 撤销选择 + track.removeLast(); + } } } ``` -可以看见,对 `res` 更新的位置处在前序遍历,也就是说,**`res` 就是树上的所有节点**: +这样,标准的组合问题也解决了。 -![](../pictures/子集/1.jpg) +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
-### 二、组合 +## 排列(元素无重不可复选) -输入两个数字 `n, k`,算法输出 `[1..n]` 中 k 个数字的所有组合。 +排列问题在前文 [回溯算法核心框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) 讲过,这里就简单过一下。 -```cpp -vector> combine(int n, int k); +力扣第 46 题「全排列」就是标准的排列问题: + +给定一个**不含重复数字**的数组 `nums`,返回其所有可能的**全排列**。 + +函数签名如下: + +```java +List> permute(int[] nums) ``` -比如输入 `n = 4, k = 2`,输出如下结果,顺序无所谓,但是不能包含重复(按照组合的定义,`[1,2]` 和 `[2,1]` 也算重复): +比如输入 `nums = [1,2,3]`,函数的返回值应该是: +```java [ - [1,2], - [1,3], - [1,4], - [2,3], - [2,4], - [3,4] + [1,2,3],[1,3,2], + [2,1,3],[2,3,1], + [3,1,2],[3,2,1] ] +``` -这也是典型的回溯算法,`k` 限制了树的高度,`n` 限制了树的宽度,继续套我们以前讲过的回溯算法模板框架就行了: -![](../pictures/子集/2.jpg) -```cpp -vector>res; +刚才讲的组合/子集问题使用 `start` 变量保证元素 `nums[start]` 之后只会出现 `nums[start+1..]` 中的元素,通过固定元素的相对位置保证不出现重复的子集。 -vector> combine(int n, int k) { - if (k <= 0 || n <= 0) return res; - vector track; - backtrack(n, k, 1, track); - return res; +**但排列问题本身就是让你穷举元素的位置,`nums[i]` 之后也可以出现 `nums[i]` 左边的元素,所以之前的那一套玩不转了,需要额外使用 `used` 数组来标记哪些元素还可以被选择**。 + +标准全排列可以抽象成如下这棵多叉树: + +![](https://labuladong.online/algo/images/permutation/7.jpeg) + +我们用 `used` 数组标记已经在路径上的元素避免重复选择,然后收集所有叶子节点上的值,就是所有全排列的结果: + +```java +class Solution { + + List> res = new LinkedList<>(); + // 记录回溯算法的递归路径 + LinkedList track = new LinkedList<>(); + // track 中的元素会被标记为 true + boolean[] used; + + // 主函数,输入一组不重复的数字,返回它们的全排列 + public List> permute(int[] nums) { + used = new boolean[nums.length]; + backtrack(nums); + return res; + } + + // 回溯算法核心函数 + void backtrack(int[] nums) { + // base case,到达叶子节点 + if (track.size() == nums.length) { + // 收集叶子节点上的值 + res.add(new LinkedList(track)); + return; + } + + // 回溯算法标准框架 + for (int i = 0; i < nums.length; i++) { + // 已经存在 track 中的元素,不能重复选择 + if (used[i]) { + continue; + } + // 做选择 + used[i] = true; + track.addLast(nums[i]); + // 进入下一层回溯树 + backtrack(nums); + // 取消选择 + track.removeLast(); + used[i] = false; + } + } } +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
-void backtrack(int n, int k, int start, vector& track) { - // 到达树的底部 - if (k == track.size()) { - res.push_back(track); + + +这样,全排列问题就解决了。 + +但如果题目不让你算全排列,而是让你算元素个数为 `k` 的排列,怎么算? + +也很简单,改下 `backtrack` 函数的 base case,仅收集第 `k` 层的节点值即可: + +```java +// 回溯算法核心函数 +void backtrack(int[] nums, int k) { + // base case,到达第 k 层,收集节点的值 + if (track.size() == k) { + // 第 k 层节点的值就是大小为 k 的排列 + res.add(new LinkedList(track)); return; } - // 注意 i 从 start 开始递增 - for (int i = start; i <= n; i++) { - // 做选择 - track.push_back(i); - backtrack(n, k, i + 1, track); - // 撤销选择 - track.pop_back(); + + // 回溯算法标准框架 + for (int i = 0; i < nums.length; i++) { + // ... + backtrack(nums, k); + // ... } } ``` -`backtrack` 函数和计算子集的差不多,区别在于,更新 `res` 的时机是树到达底端时。 +## 子集/组合(元素可重不可复选) + +刚才讲的标准子集问题输入的 `nums` 是没有重复元素的,但如果存在重复元素,怎么处理呢? -### 三、排列 +力扣第 90 题「子集 II」就是这样一个问题: -输入一个**不包含重复数字**的数组 `nums`,返回这些数字的全部排列。 +给你一个整数数组 `nums`,其中可能包含重复元素,请你返回该数组所有可能的子集。 -```cpp -vector> permute(vector& nums); +函数签名如下: + +```java +List> subsetsWithDup(int[] nums) ``` -比如说输入数组 `[1,2,3]`,输出结果应该如下,顺序无所谓,不能有重复: +比如输入 `nums = [1,2,2]`,你应该输出: -[ - [1,2,3], - [1,3,2], - [2,1,3], - [2,3,1], - [3,1,2], - [3,2,1] +```java +[ [],[1],[2],[1,2],[2,2],[1,2,2] ] +``` + +当然,按道理说「集合」不应该包含重复元素的,但既然题目这样问了,我们就忽略这个细节吧,仔细思考一下这道题怎么做才是正事。 + +就以 `nums = [1,2,2]` 为例,为了区别两个 `2` 是不同元素,后面我们写作 `nums = [1,2,2']`。 + +按照之前的思路画出子集的树形结构,显然,两条值相同的相邻树枝会产生重复: + +![](https://labuladong.online/algo/images/permutation/8.jpeg) + +```text +[ + [], + [1],[2],[2'], + [1,2],[1,2'],[2,2'], + [1,2,2'] ] +``` + +你可以看到,`[2]` 和 `[1,2]` 这两个结果出现了重复,所以我们需要进行剪枝,如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历: + +![](https://labuladong.online/algo/images/permutation/9.jpeg) + +**体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 `nums[i] == nums[i-1]`,则跳过**: + +```java +class Solution { + + List> res = new LinkedList<>(); + LinkedList track = new LinkedList<>(); + + public List> subsetsWithDup(int[] nums) { + // 先排序,让相同的元素靠在一起 + Arrays.sort(nums); + backtrack(nums, 0); + return res; + } + + void backtrack(int[] nums, int start) { + // 前序位置,每个节点的值都是一个子集 + res.add(new LinkedList<>(track)); + + for (int i = start; i < nums.length; i++) { + // 剪枝逻辑,值相同的相邻树枝,只遍历第一条 + if (i > start && nums[i] == nums[i - 1]) { + continue; + } + track.addLast(nums[i]); + backtrack(nums, i + 1); + track.removeLast(); + } + } +} +``` -「回溯算法详解」中就是拿这个问题来解释回溯模板的。这里又列出这个问题,是将「排列」和「组合」这两个回溯算法的代码拿出来对比。 -首先画出回溯树来看一看: +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
-![](../pictures/子集/3.jpg) -我们当时使用 Java 代码写的解法: + +这段代码和之前标准的子集问题的代码几乎相同,就是添加了排序和剪枝的逻辑。 + +至于为什么要这样剪枝,结合前面的图应该也很容易理解,这样带重复元素的子集问题也解决了。 + +**我们说了组合问题和子集问题是等价的**,所以我们直接看一道组合的题目吧,这是力扣第 40 题「组合总和 II」: + +给你输入 `candidates` 和一个目标和 `target`,从 `candidates` 中找出中所有和为 `target` 的组合。 + +`candidates` 可能存在重复元素,且其中的每个数字最多只能使用一次。 + +说这是一个组合问题,其实换个问法就变成子集问题了:请你计算 `candidates` 中所有和为 `target` 的子集。 + +所以这题怎么做呢? + +对比子集问题的解法,只要额外用一个 `trackSum` 变量记录回溯路径上的元素和,然后将 base case 改一改即可解决这道题: + +```java +class Solution { + + List> res = new LinkedList<>(); + // 记录回溯的路径 + LinkedList track = new LinkedList<>(); + // 记录 track 中的元素之和 + int trackSum = 0; + + public List> combinationSum2(int[] candidates, int target) { + if (candidates.length == 0) { + return res; + } + // 先排序,让相同的元素靠在一起 + Arrays.sort(candidates); + backtrack(candidates, 0, target); + return res; + } + + // 回溯算法主函数 + void backtrack(int[] nums, int start, int target) { + // base case,达到目标和,找到符合条件的组合 + if (trackSum == target) { + res.add(new LinkedList<>(track)); + return; + } + // base case,超过目标和,直接结束 + if (trackSum > target) { + return; + } + + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 剪枝逻辑,值相同的树枝,只遍历第一条 + if (i > start && nums[i] == nums[i - 1]) { + continue; + } + // 做选择 + track.add(nums[i]); + trackSum += nums[i]; + // 递归遍历下一层回溯树 + backtrack(nums, i + 1, target); + // 撤销选择 + track.removeLast(); + trackSum -= nums[i]; + } + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +## 排列(元素可重不可复选) + +排列问题的输入如果存在重复,比子集/组合问题稍微复杂一点,我们看看力扣第 47 题「全排列 II」: + +给你输入一个可包含重复数字的序列 `nums`,请你写一个算法,返回所有可能的全排列,函数签名如下: ```java -List> res = new LinkedList<>(); +List> permuteUnique(int[] nums) +``` + +比如输入 `nums = [1,2,2]`,函数返回: + +```java +[ [1,2,2],[2,1,2],[2,2,1] ] +``` + +先看解法代码: -/* 主函数,输入一组不重复的数字,返回它们的全排列 */ -List> permute(int[] nums) { - // 记录「路径」 +```java +class Solution { + + List> res = new LinkedList<>(); LinkedList track = new LinkedList<>(); - backtrack(nums, track); - return res; + boolean[] used; + + public List> permuteUnique(int[] nums) { + // 先排序,让相同的元素靠在一起 + Arrays.sort(nums); + used = new boolean[nums.length]; + backtrack(nums); + return res; + } + + void backtrack(int[] nums) { + if (track.size() == nums.length) { + res.add(new LinkedList(track)); + return; + } + + for (int i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + track.add(nums[i]); + used[i] = true; + backtrack(nums); + track.removeLast(); + used[i] = false; + } + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +你对比一下之前的标准全排列解法代码,这段解法代码只有两处不同: + +1、对 `nums` 进行了排序。 + +2、添加了一句额外的剪枝逻辑。 + +类比输入包含重复元素的子集/组合问题,你大概应该理解这么做是为了防止出现重复结果。 + +但是注意排列问题的剪枝逻辑,和子集/组合问题的剪枝逻辑略有不同:新增了 `!used[i - 1]` 的逻辑判断。 + +这个地方理解起来就需要一些技巧性了,且听我慢慢到来。为了方便研究,依然把相同的元素用上标 `'` 以示区别。 + +假设输入为 `nums = [1,2,2']`,标准的全排列算法会得出如下答案: + + + + + +``` +[ + [1,2,2'],[1,2',2], + [2,1,2'],[2,2',1], + [2',1,2],[2',2,1] +] +``` + + + +显然,这个结果存在重复,比如 `[1,2,2']` 和 `[1,2',2]` 应该只被算作同一个排列,但被算作了两个不同的排列。 + +所以现在的关键在于,如何设计剪枝逻辑,把这种重复去除掉? + +**答案是,保证相同元素在排列中的相对位置保持不变**。 + +比如说 `nums = [1,2,2']` 这个例子,我保持排列中 `2` 一直在 `2'` 前面。 + +这样的话,你从上面 6 个排列中只能挑出 3 个排列符合这个条件: + +``` +[ [1,2,2'],[2,1,2'],[2,2',1] ] +``` + +这也就是正确答案。 + +进一步,如果 `nums = [1,2,2',2'']`,我只要保证重复元素 `2` 的相对位置固定,比如说 `2 -> 2' -> 2''`,也可以得到无重复的全排列结果。 + +仔细思考,应该很容易明白其中的原理: + +**标准全排列算法之所以出现重复,是因为把相同元素形成的排列序列视为不同的序列,但实际上它们应该是相同的;而如果固定相同元素形成的序列顺序,当然就避免了重复**。 + +那么反映到代码上,你注意看这个剪枝逻辑: + + + + + +```java +// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 +if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + // 如果前面的相邻相等元素没有用过,则跳过 + continue; } +// 选择 nums[i] +``` + + + +**当出现重复元素时,比如输入 `nums = [1,2,2',2'']`,`2'` 只有在 `2` 已经被使用的情况下才会被选择,同理,`2''` 只有在 `2'` 已经被使用的情况下才会被选择,这就保证了相同元素在排列中的相对位置保证固定**。 + +这里拓展一下,如果你把上述剪枝逻辑中的 `!used[i - 1]` 改成 `used[i - 1]`,其实也可以通过所有测试用例,但效率会有所下降,这是为什么呢? + +之所以这样修改不会产生错误,是因为这种写法相当于维护了 `2'' -> 2' -> 2` 的相对顺序,最终也可以实现去重的效果。 + +但为什么这样写效率会下降呢?因为这个写法剪掉的树枝不够多。 +比如输入 `nums = [2,2',2'']`,产生的回溯树如下: + +![](https://labuladong.online/algo/images/permutation/12.jpeg) + +如果用绿色树枝代表 `backtrack` 函数遍历过的路径,红色树枝代表剪枝逻辑的触发,那么 `!used[i - 1]` 这种剪枝逻辑得到的回溯树长这样: + +![](https://labuladong.online/algo/images/permutation/13.jpeg) + +而 `used[i - 1]` 这种剪枝逻辑得到的回溯树如下: + +![](https://labuladong.online/algo/images/permutation/14.jpeg) + +可以看到,`!used[i - 1]` 这种剪枝逻辑剪得干净利落,而 `used[i - 1]` 这种剪枝逻辑虽然也能得到无重结果,但它剪掉的树枝较少,存在的无效计算较多,所以效率会差一些。 + +你可以使用可视化面板的「编辑」按钮自行修改代码验证一下,看看两种写法产生的回溯树有何差别: + + +
+ +
+ +👾 代码可视化动画👾 + +
+
+
+ +当然,关于排列去重,也有读者提出别的剪枝思路: + +```java void backtrack(int[] nums, LinkedList track) { - // 触发结束条件 if (track.size() == nums.length) { res.add(new LinkedList(track)); return; } - + + // 记录之前树枝上元素的值 + // 题目说 -10 <= nums[i] <= 10,所以初始化为特殊值 + int prevNum = -666; for (int i = 0; i < nums.length; i++) { // 排除不合法的选择 - if (track.contains(nums[i])) + if (used[i]) { continue; - // 做选择 + } + if (nums[i] == prevNum) { + continue; + } + track.add(nums[i]); - // 进入下一层决策树 + used[i] = true; + // 记录这条树枝上的值 + prevNum = nums[i]; + backtrack(nums, track); - // 取消选择 + track.removeLast(); + used[i] = false; } } ``` -回溯模板依然没有变,但是根据排列问题和组合问题画出的树来看,排列问题的树比较对称,而组合问题的树越靠右节点越少。 +这个思路也是对的,设想一个节点出现了相同的树枝: + +![](https://labuladong.online/algo/images/permutation/11.jpeg) + +如果不作处理,这些相同树枝下面的子树也会长得一模一样,所以会出现重复的排列。 + +因为排序之后所有相等的元素都挨在一起,所以只要用 `prevNum` 记录前一条树枝的值,就可以避免遍历值相同的树枝,从而避免产生相同的子树,最终避免出现重复的排列。 + +好了,这样包含重复输入的排列问题也解决了。 + +## 子集/组合(元素无重可复选) + +终于到了最后一种类型了:输入数组无重复元素,但每个元素可以被无限次使用。 + +直接看力扣第 39 题「组合总和」: + +给你一个无重复元素的整数数组 `candidates` 和一个目标和 `target`,找出 `candidates` 中可以使数字和为目标数 `target` 的所有组合。`candidates` 中的每个数字可以无限制重复被选取。 + +函数签名如下: + +```java +List> combinationSum(int[] candidates, int target) +``` + +比如输入 `candidates = [1,2,3], target = 3`,算法应该返回: + +``` +[ [1,1,1],[1,2],[3] ] +``` + +这道题说是组合问题,实际上也是子集问题:`candidates` 的哪些子集的和为 `target`? + +想解决这种类型的问题,也得回到回溯树上,**我们不妨先思考思考,标准的子集/组合问题是如何保证不重复使用元素的**? + +答案在于 `backtrack` 递归时输入的参数 `start`: + +```java +// 无重组合的回溯算法框架 +void backtrack(int[] nums, int start) { + for (int i = start; i < nums.length; i++) { + // ... + // 递归遍历下一层回溯树,注意参数 + backtrack(nums, i + 1); + // ... + } +} +``` + +这个 `i` 从 `start` 开始,那么下一层回溯树就是从 `start + 1` 开始,从而保证 `nums[start]` 这个元素不会被重复使用: + +![](https://labuladong.online/algo/images/permutation/1.jpeg) + +那么反过来,如果我想让每个元素被重复使用,我只要把 `i + 1` 改成 `i` 即可: + +```java +// 可重组合的回溯算法框架 +void backtrack(int[] nums, int start) { + for (int i = start; i < nums.length; i++) { + // ... + // 递归遍历下一层回溯树,注意参数 + backtrack(nums, i); + // ... + } +} +``` + +这相当于给之前的回溯树添加了一条树枝,在遍历这棵树的过程中,一个元素可以被无限次使用: + +![](https://labuladong.online/algo/images/permutation/10.jpeg) + +当然,这样这棵回溯树会永远生长下去,所以我们的递归函数需要设置合适的 base case 以结束算法,即路径和大于 `target` 时就没必要再遍历下去了。 + +这道题的解法代码如下: + +```java +class Solution { + + List> res = new LinkedList<>(); + // 记录回溯的路径 + LinkedList track = new LinkedList<>(); + // 记录 track 中的路径和 + int trackSum = 0; + + public List> combinationSum(int[] candidates, int target) { + if (candidates.length == 0) { + return res; + } + backtrack(candidates, 0, target); + return res; + } + + // 回溯算法主函数 + void backtrack(int[] nums, int start, int target) { + // base case,找到目标和,记录结果 + if (trackSum == target) { + res.add(new LinkedList<>(track)); + return; + } + // base case,超过目标和,停止向下遍历 + if (trackSum > target) { + return; + } + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 选择 nums[i] + trackSum += nums[i]; + track.add(nums[i]); + // 递归遍历下一层回溯树 + backtrack(nums, i, target); + // 同一元素可重复使用,注意参数 + // 撤销选择 nums[i] + trackSum -= nums[i]; + track.removeLast(); + } + } +} +``` + + +
+ +
+ +🍭 代码可视化动画🍭 + +
+
+
+ + + +## 排列(元素无重可复选) + +力扣上没有题目直接考察这个场景,我们不妨先想一下,`nums` 数组中的元素无重复且可复选的情况下,会有哪些排列? + +比如输入 `nums = [1,2,3]`,那么这种条件下的全排列共有 3^3 = 27 种: + +```java +[ + [1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3], + [2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3], + [3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3] +] +``` + +**标准的全排列算法利用 `used` 数组进行剪枝,避免重复使用同一个元素。如果允许重复使用元素的话,直接放飞自我,去除所有 `used` 数组的剪枝逻辑就行了**。 + +那这个问题就简单了,代码如下: + +```java +class Solution { + + List> res = new LinkedList<>(); + LinkedList track = new LinkedList<>(); + + public List> permuteRepeat(int[] nums) { + backtrack(nums); + return res; + } + + // 回溯算法核心函数 + void backtrack(int[] nums) { + // base case,到达叶子节点 + if (track.size() == nums.length) { + // 收集叶子节点上的值 + res.add(new LinkedList(track)); + return; + } + + // 回溯算法标准框架 + for (int i = 0; i < nums.length; i++) { + // 做选择 + track.add(nums[i]); + // 进入下一层回溯树 + backtrack(nums); + // 取消选择 + track.removeLast(); + } + } +} +``` + +至此,排列/组合/子集问题的九种变化就都讲完了。 + +## 最后总结 + +来回顾一下排列/组合/子集问题的三种形式在代码上的区别。 + +由于子集问题和组合问题本质上是一样的,无非就是 base case 有一些区别,所以把这两个问题放在一起看。 + +**形式一、元素无重不可复选,即 `nums` 中的元素都是唯一的,每个元素最多只能被使用一次**,`backtrack` 核心代码如下: + +```java +// 组合/子集问题回溯算法框架 +void backtrack(int[] nums, int start) { + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 做选择 + track.addLast(nums[i]); + // 注意参数 + backtrack(nums, i + 1); + // 撤销选择 + track.removeLast(); + } +} + +// 排列问题回溯算法框架 +void backtrack(int[] nums) { + for (int i = 0; i < nums.length; i++) { + // 剪枝逻辑 + if (used[i]) { + continue; + } + // 做选择 + used[i] = true; + track.addLast(nums[i]); + + backtrack(nums); + // 撤销选择 + track.removeLast(); + used[i] = false; + } +} +``` + +**形式二、元素可重不可复选,即 `nums` 中的元素可以存在重复,每个元素最多只能被使用一次**,其关键在于排序和剪枝,`backtrack` 核心代码如下: + +```java +Arrays.sort(nums); +// 组合/子集问题回溯算法框架 +void backtrack(int[] nums, int start) { + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 剪枝逻辑,跳过值相同的相邻树枝 + if (i > start && nums[i] == nums[i - 1]) { + continue; + } + // 做选择 + track.addLast(nums[i]); + // 注意参数 + backtrack(nums, i + 1); + // 撤销选择 + track.removeLast(); + } +} + + +Arrays.sort(nums); +// 排列问题回溯算法框架 +void backtrack(int[] nums) { + for (int i = 0; i < nums.length; i++) { + // 剪枝逻辑 + if (used[i]) { + continue; + } + // 剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + // 做选择 + used[i] = true; + track.addLast(nums[i]); + + backtrack(nums); + // 撤销选择 + track.removeLast(); + used[i] = false; + } +} +``` + +**形式三、元素无重可复选,即 `nums` 中的元素都是唯一的,每个元素可以被使用若干次**,只要删掉去重逻辑即可,`backtrack` 核心代码如下: + +```java +// 组合/子集问题回溯算法框架 +void backtrack(int[] nums, int start) { + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 做选择 + track.addLast(nums[i]); + // 注意参数 + backtrack(nums, i); + // 撤销选择 + track.removeLast(); + } +} + +// 排列问题回溯算法框架 +void backtrack(int[] nums) { + for (int i = 0; i < nums.length; i++) { + // 做选择 + track.addLast(nums[i]); + backtrack(nums); + // 撤销选择 + track.removeLast(); + } +} +``` + +只要从树的角度思考,这些问题看似复杂多变,实则改改 base case 就能解决,这也是为什么我在 [学习算法和数据结构的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) 和 [手把手刷二叉树(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) 中强调树类型题目重要性的原因。 + +如果你能够看到这里,真得给你鼓掌,相信你以后遇到各种乱七八糟的算法题,也能一眼看透它们的本质,以不变应万变。另外,考虑到篇幅,本文并没有对这些算法进行复杂度的分析,你可以使用我在 [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) 讲到的复杂度分析方法尝试自己分析它们的复杂度。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】回溯算法经典习题 I](https://labuladong.online/algo/problem-set/backtrack-i/) + - [【强化练习】回溯算法经典习题 II](https://labuladong.online/algo/problem-set/backtrack-ii/) + - [【强化练习】回溯算法经典习题 III](https://labuladong.online/algo/problem-set/backtrack-iii/) + - [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + - [动态规划和回溯算法的思维转换](https://labuladong.online/algo/dynamic-programming/word-break/) + - [回溯算法解题套路框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) + - [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/) + - [球盒模型:回溯算法穷举的两种视角](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack/) + - [算法时空复杂度分析实用指南](https://labuladong.online/algo/essential-technique/complexity-analysis/) + - [解答回溯算法/DFS算法的若干疑问](https://labuladong.online/algo/essential-technique/backtrack-vs-dfs/) + +

+ -在代码中的体现就是,排列问题每次通过 `contains` 方法来排除在 `track` 中已经选择过的数字;而组合问题通过传入一个 `start` 参数,来排除 `start` 索引之前的数字。 -**以上,就是排列组合和子集三个问题的解法,总结一下**: -子集问题可以利用数学归纳思想,假设已知一个规模较小的问题的结果,思考如何推导出原问题的结果。也可以用回溯算法,要用 `start` 参数排除已选择的数字。 +
+
+引用本文的题目 -组合问题利用的是回溯思想,结果可以表示成树结构,我们只要套用回溯算法模板即可,关键点在于要用一个 `start` 排除已经选择过的数字。 +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: -排列问题是回溯思想,也可以表示成树结构套用算法模板,关键点在于使用 `contains` 方法排除已经选择的数字,前文有详细分析,这里主要是和组合问题作对比。 +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1079. Letter Tile Possibilities](https://leetcode.com/problems/letter-tile-possibilities/?show=1) | [1079. 活字印刷](https://leetcode.cn/problems/letter-tile-possibilities/?show=1) | 🟠 | +| [131. Palindrome Partitioning](https://leetcode.com/problems/palindrome-partitioning/?show=1) | [131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/?show=1) | 🟠 | +| [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number/?show=1) | [17. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/?show=1) | 🟠 | +| [254. Factor Combinations](https://leetcode.com/problems/factor-combinations/?show=1)🔒 | [254. 因子的组合](https://leetcode.cn/problems/factor-combinations/?show=1)🔒 | 🟠 | +| [267. Palindrome Permutation II](https://leetcode.com/problems/palindrome-permutation-ii/?show=1)🔒 | [267. 回文排列 II](https://leetcode.cn/problems/palindrome-permutation-ii/?show=1)🔒 | 🟠 | +| [368. Largest Divisible Subset](https://leetcode.com/problems/largest-divisible-subset/?show=1) | [368. 最大整除子集](https://leetcode.cn/problems/largest-divisible-subset/?show=1) | 🟠 | +| [491. Non-decreasing Subsequences](https://leetcode.com/problems/non-decreasing-subsequences/?show=1) | [491. 递增子序列](https://leetcode.cn/problems/non-decreasing-subsequences/?show=1) | 🟠 | +| [638. Shopping Offers](https://leetcode.com/problems/shopping-offers/?show=1) | [638. 大礼包](https://leetcode.cn/problems/shopping-offers/?show=1) | 🟠 | +| [967. Numbers With Same Consecutive Differences](https://leetcode.com/problems/numbers-with-same-consecutive-differences/?show=1) | [967. 连续差相同的数字](https://leetcode.cn/problems/numbers-with-same-consecutive-differences/?show=1) | 🟠 | +| [996. Number of Squareful Arrays](https://leetcode.com/problems/number-of-squareful-arrays/?show=1) | [996. 正方形数组的数目](https://leetcode.cn/problems/number-of-squareful-arrays/?show=1) | 🔴 | +| - | [剑指 Offer 38. 字符串的排列](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/?show=1) | 🟠 | +| - | [剑指 Offer II 079. 所有子集](https://leetcode.cn/problems/TVdhkn/?show=1) | 🟠 | +| - | [剑指 Offer II 080. 含有 k 个元素的组合](https://leetcode.cn/problems/uUsW3B/?show=1) | 🟠 | +| - | [剑指 Offer II 081. 允许重复选择元素的组合](https://leetcode.cn/problems/Ygoe9J/?show=1) | 🟠 | +| - | [剑指 Offer II 083. 没有重复元素集合的全排列](https://leetcode.cn/problems/VvJkup/?show=1) | 🟠 | -记住这几种树的形状,就足以应对大部分回溯算法问题了,无非就是 `start` 或者 `contains` 剪枝,也没啥别的技巧了。 +
+
-坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:回溯算法详解](../算法思维系列/回溯算法详解修订版.md) -[下一篇:二分查找详解](../算法思维系列/二分查找详解.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" new file mode 100644 index 0000000000..69bc2b22a3 --- /dev/null +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" @@ -0,0 +1,243 @@ +# 扫描线技巧:安排会议室 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [253. Meeting Rooms II](https://leetcode.com/problems/meeting-rooms-ii/)🔒 | [253. 会议室 II](https://leetcode.cn/problems/meeting-rooms-ii/)🔒 | 🟠 | + +**-----------** + + + +之前面试,被问到一道非常经典且非常实用的算法题目:会议室安排问题。 + +力扣上类似的问题是会员题目,你可能没办法做,但对于这种经典的算法题,掌握思路还是必要的。 + +先说下题目,力扣第 253 题「会议室 II」: + +给你输入若干形如 `[begin, end]` 的区间,代表若干会议的开始时间和结束时间,请你计算至少需要申请多少间会议室。 + +函数签名如下: + +```java +// 返回需要申请的会议室数量 +int minMeetingRooms(int[][] meetings); +``` + +比如给你输入 `meetings = [[0,30],[5,10],[15,20]]`,算法应该返回 2,因为后两个会议和第一个会议时间是冲突的,至少申请两个会议室才能让所有会议顺利进行。 + +如果会议之间的时间有重叠,那就得额外申请会议室来开会,想求至少需要多少间会议室,就是让你计算同一时刻最多有多少会议在同时进行。 + +换句话说,**如果把每个会议的起始时间看做一个线段区间,那么题目就是让你求最多有几个重叠区间**,仅此而已。 + +我们之前也学习过区间相关的算法,如果你对 [差分数组技巧](https://labuladong.online/algo/data-structure/diff-array/) 有印象,应该首先能想到用那个技巧来解决这个题。 + +这道题相当于是说,给你一个原本全是 0 的数组,然后给你若干区间,让你对每个区间中的元素都加 1,问你最后整个数组中的最大值是多少。这就是经典的差分数组实用场景对吧,直接套用前文给的 `Difference` 类就可以解决这个问题了。 + +但是差分数组技巧有一个问题,就是你必须把那个全是 0 的初始数组构造出来。由于我们用数组的索引表示时间,所以这个数组的长度取决于时间区间的最大值。 + +比如输入 `meetings = [[0,30],[5,10],[15,20]]`,那么你得构造一个长度为 30 的数组。那如果输入 `meetings = [[0,30],[5,10],[10^8,10^9]]`,这样的话你就得构造一个长度为 10^9 的数组,这显然是有问题的。不过这道题给的数据规模是时间的取值最多为 10^6,不算是特别大,用差分数组的方法应该可以通过。 + +但本文再教你另外的一个处理区间的技巧,不用构造这么大的数组,也能巧妙解决这个问题。 + + + + + + + +## 题目延伸 + +我们之前写过很多区间调度相关的文章,这里就顺便帮大家梳理一下这类问题的思路: + +**第一个场景**,假设现在只有一个会议室,还有若干会议,你如何将尽可能多的会议安排到这个会议室里? + +这个问题需要将这些会议(区间)按结束时间(右端点)排序,然后进行处理,详见前文 [贪心算法做时间管理](https://labuladong.online/algo/frequency-interview/interval-scheduling/)。 + +**第二个场景**,给你若干较短的视频片段,和一个较长的视频片段,请你从较短的片段中尽可能少地挑出一些片段,拼接出较长的这个片段。 + +这个问题需要将这些视频片段(区间)按开始时间(左端点)排序,然后进行处理,详见前文 [剪视频剪出一个贪心算法](https://labuladong.online/algo/frequency-interview/cut-video/)。 + +**第三个场景**,给你若干区间,其中可能有些区间比较短,被其他区间完全覆盖住了,请你删除这些被覆盖的区间。 + +这个问题需要将这些区间按左端点排序,然后就能找到并删除那些被完全覆盖的区间了,详见前文 [删除覆盖区间](https://labuladong.online/algo/practice-in-action/interval-problem-summary/)。 + +**第四个场景**,给你若干区间,请你将所有有重叠部分的区间进行合并。 + +这个问题需要将这些区间按左端点排序,方便找出存在重叠的区间,详见前文 [合并重叠区间](https://labuladong.online/algo/practice-in-action/interval-problem-summary/)。 + +**第五个场景**,有两个部门同时预约了同一个会议室的若干时间段,请你计算会议室的冲突时段。 + +这个问题就是给你两组区间列表,请你找出这两组区间的交集,这需要你将这些区间按左端点排序,详见前文 [区间交集问题](https://labuladong.online/algo/practice-in-action/interval-problem-summary/)。 + +**第六个场景**,假设现在只有一个会议室,还有若干会议,如何安排会议才能使这个会议室的闲置时间最少? + +这个问题需要动动脑筋,说白了这就是个 0-1 背包问题的变形: + +会议室可以看做一个背包,每个会议可以看做一个物品,物品的价值就是会议的时长,请问你如何选择物品(会议)才能最大化背包中的价值(会议室的使用时长)? + +当然,这里背包的约束不是一个最大重量,而是各个物品(会议)不能互相冲突。把各个会议按照结束时间进行排序,然后参考前文 [0-1 背包问题详解](https://labuladong.online/algo/dynamic-programming/knapsack1/) 的思路和 TreeMap 即可解决。 + +力扣第 1235 题「规划兼职工作」就是类似的题目,我在插件思路中给出了详细的解答,你可以安装我的 [Chrome 插件](https://labuladong.online/algo/intro/chrome/) 去查看,我在这里就不花费篇幅了。 + +**第七个场景**,就是本文想讲的场景,给你若干会议,让你最小化申请会议室的数量。 + +好了,举例了这么多,来看看今天的这个问题如何解决。 + + + + + + + +## 题目分析 + +重复一下题目的本质: + +**给你输入若干时间区间,让你计算同一时刻「最多」有几个区间重叠**。 + +题目的关键点在于,给你任意一个时刻,你是否能够说出这个时刻有几个会议? + +如果可以做到,那我遍历所有的时刻,找个最大值,就是需要申请的会议室数量。 + +有没有一种数据结构或者算法,给我输入若干区间,我能知道每个位置有多少个区间重叠? + +老读者肯定可以联想到之前说过的一个算法技巧:[差分数组技巧](https://labuladong.online/algo/data-structure/diff-array/)。 + +把时间线想象成一个初始值为 0 的数组,每个时间区间 `[i, j]` 就相当于一个子数组,这个时间区间有一个会议,那我就把这个子数组中的元素都加一。 + +最后,每个时刻有几个会议我不就知道了吗?我遍历整个数组,不就知道至少需要几间会议室了吗? + +举例来说,如果输入 `meetings = [[0,30],[5,10],[15,20]]`,那么我们就给数组中 `[0,30],[5,10],[15,20]` 这几个索引区间分别加一,最后遍历数组,求个最大值就行了。 + +还记得吗,差分数组技巧可以在 O(1) 时间对整个区间的元素进行加减,所以可以拿来解决这道题。 + +不过,这个解法的效率不算高,所以我这里不准备具体写差分数组的解法,参照 [差分数组技巧](https://labuladong.online/algo/data-structure/diff-array/) 的原理,有兴趣的读者可以自己尝试去实现。 + + + + + + + +**基于差分数组的思路,我们可以推导出一种更高效,更优雅的解法**。 + +我们首先把这些会议的时间区间进行投影: + +![](https://labuladong.online/algo/images/arrange-room/1.jpeg) + +红色的点代表每个会议的开始时间点,绿色的点代表每个会议的结束时间点。 + +现在假想有一条带着计数器的线,在时间线上从左至右进行扫描,每遇到红色的点,计数器 `count` 加一,每遇到绿色的点,计数器 `count` 减一: + +![](https://labuladong.online/algo/images/arrange-room/2.jpeg) + +**这样一来,每个时刻有多少个会议在同时进行,就是计数器 `count` 的值,`count` 的最大值,就是需要申请的会议室数量**。 + +对差分数组技巧熟悉的读者一眼就能看出来了,这个扫描线其实就是差分数组的遍历过程,所以我们说这是差分数组技巧衍生出来的解法。 + +## 代码实现 + +那么,如何写代码实现这个扫描的过程呢? + +首先,对区间进行投影,就相当于对每个区间的起点和终点分别进行排序: + +![](https://labuladong.online/algo/images/arrange-room/3.jpeg) + +```java +int minMeetingRooms(int[][] meetings) { + int n = meetings.length; + int[] begin = new int[n]; + int[] end = new int[n]; + // 把左端点和右端点单独拿出来 + for(int i = 0; i < n; i++) { + begin[i] = meetings[i][0]; + end[i] = meetings[i][1]; + } + // 排序后就是图中的红点 + Arrays.sort(begin); + // 排序后就是图中的绿点 + Arrays.sort(end); + + // ... +} +``` + +然后就简单了,扫描线从左向右前进,遇到红点就对计数器加一,遇到绿点就对计数器减一,计数器 `count` 的最大值就是答案: + +```java +class Solution { + public int minMeetingRooms(int[][] meetings) { + int n = meetings.length; + int[] begin = new int[n]; + int[] end = new int[n]; + for(int i = 0; i < n; i++) { + begin[i] = meetings[i][0]; + end[i] = meetings[i][1]; + } + Arrays.sort(begin); + Arrays.sort(end); + + // 扫描过程中的计数器 + int count = 0; + // 双指针技巧 + int res = 0, i = 0, j = 0; + while (i < n && j < n) { + if (begin[i] < end[j]) { + // 扫描到一个红点 + count++; + i++; + } else { + // 扫描到一个绿点 + count--; + j++; + } + // 记录扫描过程中的最大值 + res = Math.max(res, count); + } + + return res; + } +} +``` + +这里使用的是 [双指针技巧](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/),根据 `i, j` 的相对位置模拟扫描线前进的过程。 + +至此,这道题就做完了。当然,这个题目也可以变形,比如给你若干会议,问你 `k` 个会议室够不够用,其实你套用本文的解法代码,也可以很轻松解决。 + + + + + + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1235. Maximum Profit in Job Scheduling](https://leetcode.com/problems/maximum-profit-in-job-scheduling/?show=1) | [1235. 规划兼职工作](https://leetcode.cn/problems/maximum-profit-in-job-scheduling/?show=1) | 🔴 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" new file mode 100644 index 0000000000..1aa72d8ebe --- /dev/null +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" @@ -0,0 +1,633 @@ +# 一文秒杀所有岛屿题目 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1020. Number of Enclaves](https://leetcode.com/problems/number-of-enclaves/) | [1020. 飞地的数量](https://leetcode.cn/problems/number-of-enclaves/) | 🟠 | +| [1254. Number of Closed Islands](https://leetcode.com/problems/number-of-closed-islands/) | [1254. 统计封闭岛屿的数目](https://leetcode.cn/problems/number-of-closed-islands/) | 🟠 | +| [1905. Count Sub Islands](https://leetcode.com/problems/count-sub-islands/) | [1905. 统计子岛屿](https://leetcode.cn/problems/count-sub-islands/) | 🟠 | +| [200. Number of Islands](https://leetcode.com/problems/number-of-islands/) | [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | 🟠 | +| [694. Number of Distinct Islands](https://leetcode.com/problems/number-of-distinct-islands/)🔒 | [694. 不同岛屿的数量](https://leetcode.cn/problems/number-of-distinct-islands/)🔒 | 🟠 | +| [695. Max Area of Island](https://leetcode.com/problems/max-area-of-island/) | [695. 岛屿的最大面积](https://leetcode.cn/problems/max-area-of-island/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [二叉树系列算法(纲领篇)](https://labuladong.online/algo/essential-technique/binary-tree-summary/) +> - [回溯算法核心框架](https://labuladong.online/algo/essential-technique/backtrack-framework/) +> - [关于回溯/DFS算法的若干疑问](https://labuladong.online/algo/essential-technique/backtrack-vs-dfs/) + +岛屿系列算法问题是经典的面试高频题,虽然基本的问题并不难,但是这类问题有一些有意思的扩展,比如求子岛屿数量,求形状不同的岛屿数量等等,本文就来把这些问题一网打尽。 + +**岛屿系列题目的核心考点就是用 DFS/BFS 算法遍历二维数组**。 + +本文主要来讲解如何用 DFS 算法来秒杀岛屿系列题目,不过用 BFS 算法的核心思路是完全一样的,无非就是把 DFS 改写成 BFS 而已。 + +那么如何在二维矩阵中使用 DFS 搜索呢?如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。 + +根据 [学习数据结构和算法的框架思维](https://labuladong.online/algo/essential-technique/algorithm-summary/),完全可以根据二叉树的遍历框架改写出二维矩阵的 DFS 代码框架: + +```java +// 二叉树遍历框架 +void traverse(TreeNode root) { + traverse(root.left); + traverse(root.right); +} + +// 二维矩阵遍历框架 +void dfs(int[][] grid, int i, int j, boolean[][] visited) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + if (visited[i][j]) { + // 已遍历过 (i, j) + return; + } + + // 进入当前节点 (i, j) + visited[i][j] = true; + + // 进入相邻节点(四叉树) + // 上 + dfs(grid, i - 1, j, visited); + // 下 + dfs(grid, i + 1, j, visited); + // 左 + dfs(grid, i, j - 1, visited); + // 右 + dfs(grid, i, j + 1, visited); +} +``` + +因为二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个 `visited` 布尔数组防止走回头路,如果你能理解上面这段代码,那么搞定所有岛屿系列题目都很简单。 + +这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文 [union-find 算法详解](https://labuladong.online/algo/data-structure/union-find/) 的代码很类似: + +```java +// 方向数组,分别代表上、下、左、右 +int[][] dirs = new int[][]{{-1,0}, {1,0}, {0,-1}, {0,1}}; + +void dfs(int[][] grid, int i, int j, boolean[][] visited) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + if (visited[i][j]) { + // 已遍历过 (i, j) + return; + } + + // 进入节点 (i, j) + visited[i][j] = true; + // 递归遍历上下左右的节点 + for (int[] d : dirs) { + int next_i = i + d[0]; + int next_j = j + d[1]; + dfs(grid, next_i, next_j, visited); + } + // 离开节点 (i, j) +} +``` + +这种写法无非就是用 for 循环处理上下左右的遍历罢了,你可以按照个人喜好选择写法。下面就按照上述框架结合可视化面板来解题。 + + + + + + + +## 岛屿数量 + +这是力扣第 200 题「岛屿数量」,最简单也是最经典的一道问题,题目会输入一个二维数组 `grid`,其中只包含 `0` 或者 `1`,`0` 代表海水,`1` 代表陆地,且假设该矩阵四周都是被海水包围着的。 + +我们说连成片的陆地形成岛屿,那么请你写一个算法,计算这个矩阵 `grid` 中岛屿的个数,函数签名如下: + +```java +int numIslands(char[][] grid); +``` + +比如说题目给你输入下面这个 `grid` 有四片岛屿,算法应该返回 4: + +![](https://labuladong.online/algo/images/island/1.jpg) + +思路很简单,关键在于如何寻找并标记「岛屿」,这就要 DFS 算法发挥作用了,我们直接看解法代码: + +```java +class Solution { + // 主函数,计算岛屿数量 + int numIslands(char[][] grid) { + int res = 0; + int m = grid.length, n = grid[0].length; + // 遍历 grid + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == '1') { + // 每发现一个岛屿,岛屿数量加一 + res++; + // 然后使用 DFS 将岛屿淹了 + dfs(grid, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(char[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + if (grid[i][j] == '0') { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = '0'; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +} +``` + + +
+ +
+ +🌟 代码可视化动画🌟 + +
+
+
+ + + +**为什么每次遇到岛屿,都要用 DFS 算法把岛屿「淹了」呢?主要是为了省事,避免维护 `visited` 数组**。 + +因为 `dfs` 函数遍历到值为 `0` 的位置会直接返回,所以只要把经过的位置都设置为 `0`,就可以起到不走回头路的作用。 + +> [!TIP] +> 这类 DFS 算法还有个别名叫做 FloodFill 算法,现在有没有觉得 FloodFill 这个名字还挺贴切的~ + +这个最最基本的算法问题就说到这,我们来看看后面的题目有什么花样。 + +## 封闭岛屿的数量 + +上一题说二维矩阵四周可以认为也是被海水包围的,所以靠边的陆地也算作岛屿。 + +力扣第 1254 题「统计封闭岛屿的数目」和上一题有两点不同: + +1、用 `0` 表示陆地,用 `1` 表示海水。 + +2、让你计算「封闭岛屿」的数目。所谓「封闭岛屿」就是上下左右全部被 `1` 包围的 `0`,也就是说**靠边的陆地不算作「封闭岛屿」**。 + +函数签名如下: + +```java +int closedIsland(int[][] grid) +``` + +比如题目给你输入如下这个二维矩阵: + +![](https://labuladong.online/algo/images/island/2.png) + +算法返回 2,只有图中灰色部分的 `0` 是四周全都被海水包围着的「封闭岛屿」。 + +**那么如何判断「封闭岛屿」呢?其实很简单,把上一题中那些靠边的岛屿排除掉,剩下的不就是「封闭岛屿」了吗**? + +有了这个思路,就可以直接看代码了,注意这题规定 `0` 表示陆地,用 `1` 表示海水: + +```java +class Solution { + // 主函数:计算封闭岛屿的数量 + public int closedIsland(int[][] grid) { + int m = grid.length, n = grid[0].length; + for (int j = 0; j < n; j++) { + // 把靠上边的岛屿淹掉 + dfs(grid, 0, j); + // 把靠下边的岛屿淹掉 + dfs(grid, m - 1, j); + } + for (int i = 0; i < m; i++) { + // 把靠左边的岛屿淹掉 + dfs(grid, i, 0); + // 把靠右边的岛屿淹掉 + dfs(grid, i, n - 1); + } + // 遍历 grid,剩下的岛屿都是封闭岛屿 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) { + res++; + dfs(grid, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 1) { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = 1; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +} +``` + + +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
+ + + +只要提前把靠边的陆地都淹掉,然后算出来的就是封闭岛屿了。 + +> [!TIP] +> 处理这类岛屿题目除了 DFS/BFS 算法之外,Union Find 并查集算法也是一种可选的方法,前文 [Union Find 算法运用](https://labuladong.online/algo/data-structure/union-find/) 就用 Union Find 算法解决了一道类似的问题。 + +这道岛屿题目的解法稍微改改就可以解决力扣第 1020 题「飞地的数量」,这题不让你求封闭岛屿的数量,而是求封闭岛屿的面积总和。 + +其实思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量就行了,非常简单。不过注意第 1020 题中 `1` 代表陆地,`0` 代表海水。 + +篇幅所限,具体代码我就不写了,我们继续看其他的岛屿题目。 + +## 岛屿的最大面积 + +这是力扣第 695 题「岛屿的最大面积」,`0` 表示海水,`1` 表示陆地,现在不让你计算岛屿的个数了,而是让你计算最大的那个岛屿的面积,函数签名如下: + +```java +int maxAreaOfIsland(int[][] grid) +``` + +比如题目给你输入如下一个二维矩阵: + +![](https://labuladong.online/algo/images/island/3.jpg) + +其中面积最大的是橘红色的岛屿,算法返回它的面积 6。 + +**这题的大体思路和之前完全一样,只不过 `dfs` 函数淹没岛屿的同时,还应该想办法记录这个岛屿的面积**。 + +我们可以给 `dfs` 函数设置返回值,记录每次淹没的陆地的个数,直接看解法吧: + +```java +class Solution { + public int maxAreaOfIsland(int[][] grid) { + // 记录岛屿的最大面积 + int res = 0; + int m = grid.length, n = grid[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹没岛屿,并更新最大岛屿面积 + res = Math.max(res, dfs(grid, i, j)); + } + } + } + return res; + } + + // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + int dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] == 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; + + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + } +} +``` + + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + +解法和之前相比差不多,我也不多说了,接下来的两道岛屿题目是比较有技巧性的,我们重点来看一下。 + +## 子岛屿数量 + +如果说前面的题目都是模板题,那么力扣第 1905 题「统计子岛屿」可能得动动脑子了: + + + +**这道题的关键在于,如何快速判断子岛屿**?肯定可以借助 [Union Find 并查集算法](https://labuladong.online/algo/data-structure/union-find/) 来判断,不过本文重点在 DFS 算法,就不展开并查集算法了。 + +什么情况下 `grid2` 中的一个岛屿 `B` 是 `grid1` 中的一个岛屿 `A` 的子岛? + +当岛屿 `B` 中所有陆地在岛屿 `A` 中也是陆地的时候,岛屿 `B` 是岛屿 `A` 的子岛。 + +**反过来说,如果岛屿 `B` 中存在一片陆地,在岛屿 `A` 的对应位置是海水,那么岛屿 `B` 就不是岛屿 `A` 的子岛**。 + +那么,我们只要遍历 `grid2` 中的所有岛屿,把那些不可能是子岛的岛屿排除掉,剩下的就是子岛。 + +依据这个思路,可以直接写出下面的代码: + +```java +class Solution { + public int countSubIslands(int[][] grid1, int[][] grid2) { + int m = grid1.length, n = grid1[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid1[i][j] == 0 && grid2[i][j] == 1) { + // 这个岛屿肯定不是子岛,淹掉 + dfs(grid2, i, j); + } + } + } + // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid2[i][j] == 1) { + res++; + dfs(grid2, i, j); + } + } + } + return res; + } + + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 0) { + return; + } + + grid[i][j] = 0; + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +这道题的思路和计算「封闭岛屿」数量的思路有些类似,只不过后者排除那些靠边的岛屿,前者排除那些不可能是子岛的岛屿。 + +## 不同的岛屿数量 + +这是本文的最后一道岛屿题目,作为压轴题,当然是最有意思的。 + +力扣第 694 题「不同的岛屿数量」,题目还是输入一个二维矩阵,`0` 表示海水,`1` 表示陆地,这次让你计算 **不同的 (distinct)** 岛屿数量,函数签名如下: + +```java +int numDistinctIslands(int[][] grid) +``` + +比如题目输入下面这个二维矩阵: + +![](https://labuladong.online/algo/images/island/5.jpg) + +其中有四个岛屿,但是左下角和右上角的岛屿形状相同,所以不同的岛屿共有三个,算法返回 3。 + +很显然我们得想办法把二维矩阵中的「岛屿」进行转化,变成比如字符串这样的类型,然后利用 HashSet 这样的数据结构去重,最终得到不同的岛屿的个数。 + +如果想把岛屿转化成字符串,说白了就是序列化,序列化说白了就是遍历嘛,前文 [二叉树的序列化和反序列化](https://labuladong.online/algo/data-structure/serialize-and-deserialize-binary-tree/) 讲了二叉树和字符串互转,这里也是类似的。 + +**首先,对于形状相同的岛屿,如果从同一起点出发,`dfs` 函数遍历的顺序肯定是一样的**。 + +因为遍历顺序是写死在你的递归函数里面的,不会动态改变: + +```java +void dfs(int[][] grid, int i, int j) { + // 递归顺序: + // 上 + dfs(grid, i - 1, j); + // 下 + dfs(grid, i + 1, j); + // 左 + dfs(grid, i, j - 1); + // 右 + dfs(grid, i, j + 1); +} +``` + +所以,遍历顺序从某种意义上说就可以用来描述岛屿的形状,比如下图这两个岛屿: + +![](https://labuladong.online/algo/images/island/6.png) + +假设它们的遍历顺序是: + +``` +下,右,上,撤销上,撤销右,撤销下 +``` + +如果我用分别用 `1, 2, 3, 4` 代表上下左右,用 `-1, -2, -3, -4` 代表上下左右的撤销,那么可以这样表示它们的遍历顺序: + +``` +2, 4, 1, -1, -4, -2 +``` + +**你看,这就相当于是岛屿序列化的结果,只要每次使用 `dfs` 遍历岛屿的时候生成这串数字进行比较,就可以计算到底有多少个不同的岛屿了**。 + +::: info 一定要记录「撤销」操作吗? + +细心的读者问到,为什么记录「撤销」操作才能唯一表示遍历顺序呢?不记录撤销操作好像也可以?不对,实际上必须记录撤销操作。 + +比方说「下,右,撤销右,撤销下」和「下,撤销下,右,撤销右」显然是两个不同的遍历顺序,但如果不记录撤销操作,那么它俩都是「下,右」,成了相同的遍历顺序,显然是不对的。 + +::: + +所以我们需要稍微改造 `dfs` 函数,添加一些函数参数以便记录遍历顺序: + +```java +void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n + || grid[i][j] == 0) { + return; + } + // 前序遍历位置:进入 (i, j) + grid[i][j] = 0; + sb.append(dir).append(','); + + // 上 + dfs(grid, i - 1, j, sb, 1); + // 下 + dfs(grid, i + 1, j, sb, 2); + // 左 + dfs(grid, i, j - 1, sb, 3); + // 右 + dfs(grid, i, j + 1, sb, 4); + + // 后序遍历位置:离开 (i, j) + sb.append(-dir).append(','); +} +``` + +> [!NOTE] +> 仔细看这个代码,在递归前做选择,在递归后撤销选择,它像不像 [回溯算法框架](https://labuladong.online/algo/essential-technique/backtrack-framework/)?实际上它就是回溯算法,因为它关注的是「树枝」(岛屿的遍历顺序),而不是「节点」(岛屿的每个格子)。 +> +> 你完全可以把这个函数改写成回溯算法的标准形式。 + +`dir` 记录方向,`dfs` 函数递归结束后,`sb` 记录着整个遍历顺序。有了这个 `dfs` 函数就好办了,我们可以直接写出最后的解法代码: + +```java +class Solution { + public int numDistinctIslands(int[][] grid) { + int m = grid.length, n = grid[0].length; + // 记录所有岛屿的序列化结果 + HashSet islands = new HashSet<>(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹掉这个岛屿,同时存储岛屿的序列化结果 + StringBuilder sb = new StringBuilder(); + // 初始的方向可以随便写,不影响正确性 + dfs(grid, i, j, sb, 666); + islands.add(sb.toString()); + } + } + } + // 不相同的岛屿数量 + return islands.size(); + } + + private void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) { + // 见上文 + } +} +``` + + +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
+ + + +这样,这道题就解决了,至于为什么初始调用 `dfs` 函数时的 `dir` 参数可以随意写,因为这个 `dfs` 函数实际上是回溯算法,它关注的是「树枝」而不是「节点」,前文 [图算法基础](https://labuladong.online/algo/data-structure-basic/graph-basic/) 有写具体的区别,这里就不赘述了。 + +以上就是全部岛屿系列题目的解题思路,也许前面的题目大部分人会做,但是最后两题还是比较巧妙的,希望本文对你有帮助。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】BFS 经典习题 II](https://labuladong.online/algo/problem-set/bfs-ii/) + - [【强化练习】回溯算法经典习题 I](https://labuladong.online/algo/problem-set/backtrack-i/) + - [【强化练习】回溯算法经典习题 II](https://labuladong.online/algo/problem-set/backtrack-ii/) + - [【强化练习】并查集经典习题](https://labuladong.online/algo/problem-set/union-find/) + - [二叉树系列算法核心纲领](https://labuladong.online/algo/essential-technique/binary-tree-summary/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [1219. Path with Maximum Gold](https://leetcode.com/problems/path-with-maximum-gold/?show=1) | [1219. 黄金矿工](https://leetcode.cn/problems/path-with-maximum-gold/?show=1) | 🟠 | +| [547. Number of Provinces](https://leetcode.com/problems/number-of-provinces/?show=1) | [547. 省份数量](https://leetcode.cn/problems/number-of-provinces/?show=1) | 🟠 | +| [79. Word Search](https://leetcode.com/problems/word-search/?show=1) | [79. 单词搜索](https://leetcode.cn/problems/word-search/?show=1) | 🟠 | +| [924. Minimize Malware Spread](https://leetcode.com/problems/minimize-malware-spread/?show=1) | [924. 尽量减少恶意软件的传播](https://leetcode.cn/problems/minimize-malware-spread/?show=1) | 🔴 | +| [面试题13. 机器人的运动范围 LCOF](https://leetcode.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/?show=1) | [面试题13. 机器人的运动范围](https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/?show=1) | 🟠 | +| - | [剑指 Offer II 105. 岛屿的最大面积](https://leetcode.cn/problems/ZL6zAn/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" index d1d2a34106..9c2b3dd4d6 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" @@ -1,6 +1,27 @@ # 如何调度考生的座位 -这是 LeetCode 第 885 题,有趣且具有一定技巧性。这种题目并不像动态规划这类算法拼智商,而是看你对常用数据结构的理解和写代码的水平,个人认为值得重视和学习。 +

+GitHub + + + +

+ +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:[新版网站会员](https://labuladong.online/algo/intro/site-vip/) 即将涨价;已支持老用户续费~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [855. Exam Room](https://leetcode.com/problems/exam-room/) | [855. 考场就座](https://leetcode.cn/problems/exam-room/) | 🟠 + +**-----------** + +本文讲一讲力扣第 855 题「考场就座」,有趣且具有一定技巧性。这种题目并不像动态规划这类算法拼智商,而是看你对常用数据结构的理解和写代码的水平,个人认为值得重视和学习。 另外说句题外话,很多读者都问,算法框架是如何总结出来的,其实框架反而是慢慢从细节里抠出来的。希望大家看了我们的文章之后,最好能抽时间把相关的问题亲自做一做,纸上得来终觉浅,绝知此事要躬行嘛。 @@ -10,6 +31,7 @@ 也就是请你实现下面这样一个类: + ```java class ExamRoom { // 构造函数,传入座位总数 N @@ -64,92 +86,101 @@ class ExamRoom { 这个问题还用到一个常用的编程技巧,就是使用一个「虚拟线段」让算法正确启动,这就和链表相关的算法需要「虚拟头结点」一个道理。 + ```java -// 将端点 p 映射到以 p 为左端点的线段 -private Map startMap; -// 将端点 p 映射到以 p 为右端点的线段 -private Map endMap; -// 根据线段长度从小到大存放所有线段 -private TreeSet pq; -private int N; - -public ExamRoom(int N) { - this.N = N; - startMap = new HashMap<>(); - endMap = new HashMap<>(); - pq = new TreeSet<>((a, b) -> { - // 算出两个线段的长度 - int distA = distance(a); - int distB = distance(b); - // 长度更长的更大,排后面 - return distA - distB; - }); - // 在有序集合中先放一个虚拟线段 - addInterval(new int[] {-1, N}); -} +class ExamRoom { + // 将端点 p 映射到以 p 为左端点的线段 + private Map startMap; + // 将端点 p 映射到以 p 为右端点的线段 + private Map endMap; + // 根据线段长度从小到大存放所有线段 + private TreeSet pq; + private int N; + + public ExamRoom(int N) { + this.N = N; + startMap = new HashMap<>(); + endMap = new HashMap<>(); + pq = new TreeSet<>((a, b) -> { + // 算出两个线段的长度 + int distA = distance(a); + int distB = distance(b); + // 长度更长的更大,排后面 + return distA - distB; + }); + // 在有序集合中先放一个虚拟线段 + addInterval(new int[] {-1, N}); + } -/* 去除一个线段 */ -private void removeInterval(int[] intv) { - pq.remove(intv); - startMap.remove(intv[0]); - endMap.remove(intv[1]); -} + /* 去除一个线段 */ + private void removeInterval(int[] intv) { + pq.remove(intv); + startMap.remove(intv[0]); + endMap.remove(intv[1]); + } -/* 增加一个线段 */ -private void addInterval(int[] intv) { - pq.add(intv); - startMap.put(intv[0], intv); - endMap.put(intv[1], intv); -} + /* 增加一个线段 */ + private void addInterval(int[] intv) { + pq.add(intv); + startMap.put(intv[0], intv); + endMap.put(intv[1], intv); + } -/* 计算一个线段的长度 */ -private int distance(int[] intv) { - return intv[1] - intv[0] - 1; + /* 计算一个线段的长度 */ + private int distance(int[] intv) { + return intv[1] - intv[0] - 1; + } + + // ... } ``` 「虚拟线段」其实就是为了将所有座位表示为一个线段: -![](../pictures/座位调度/1.jpg) +![](https://labuladong.online/algo/images/座位调度/1.jpg) 有了上述铺垫,主要 API `seat` 和 `leave` 就可以写了: ```java -public int seat() { - // 从有序集合拿出最长的线段 - int[] longest = pq.last(); - int x = longest[0]; - int y = longest[1]; - int seat; - if (x == -1) { // 情况一 - seat = 0; - } else if (y == N) { // 情况二 - seat = N - 1; - } else { // 情况三 - seat = (y - x) / 2 + x; +class ExamRoom { + // ... + + public int seat() { + // 从有序集合拿出最长的线段 + int[] longest = pq.last(); + int x = longest[0]; + int y = longest[1]; + int seat; + if (x == -1) { // 情况一 + seat = 0; + } else if (y == N) { // 情况二 + seat = N - 1; + } else { // 情况三 + seat = (y - x) / 2 + x; + } + // 将最长的线段分成两段 + int[] left = new int[] {x, seat}; + int[] right = new int[] {seat, y}; + removeInterval(longest); + addInterval(left); + addInterval(right); + return seat; } - // 将最长的线段分成两段 - int[] left = new int[] {x, seat}; - int[] right = new int[] {seat, y}; - removeInterval(longest); - addInterval(left); - addInterval(right); - return seat; -} -public void leave(int p) { - // 将 p 左右的线段找出来 - int[] right = startMap.get(p); - int[] left = endMap.get(p); - // 合并两个线段成为一个线段 - int[] merged = new int[] {left[0], right[1]}; - removeInterval(left); - removeInterval(right); - addInterval(merged); + public void leave(int p) { + // 将 p 左右的线段找出来 + int[] right = startMap.get(p); + int[] left = endMap.get(p); + // 合并两个线段成为一个线段 + int[] merged = new int[] {left[0], right[1]}; + removeInterval(left); + removeInterval(right); + addInterval(merged); + } } ``` -![三种情况](../pictures/座位调度/2.jpg) +![](https://labuladong.online/algo/images/座位调度/2.jpg) 至此,算法就基本实现了,代码虽多,但思路很简单:找最长的线段,从中间分隔成两段,中点就是 `seat()` 的返回值;找 `p` 的左右线段,合并成一个线段,这就是 `leave(p)` 的逻辑。 @@ -157,11 +188,11 @@ public void leave(int p) { 但是,题目要求多个选择时选择索引最小的那个座位,我们刚才忽略了这个问题。比如下面这种情况会出错: -![](../pictures/座位调度/3.jpg) +![](https://labuladong.online/algo/images/座位调度/3.jpg) 现在有序集合里有线段 `[0,4]` 和 `[4,9]`,那么最长线段 `longest` 就是后者,按照 `seat` 的逻辑,就会分割 `[4,9]`,也就是返回座位 6。但正确答案应该是座位 2,因为 2 和 6 都满足最大化相邻考生距离的条件,二者应该取较小的。 -![](../pictures/座位调度/4.jpg) +![](https://labuladong.online/algo/images/座位调度/4.jpg) **遇到题目的这种要求,解决方式就是修改有序数据结构的排序方式**。具体到这个问题,就是修改 `TreeMap` 的比较函数逻辑: @@ -178,18 +209,23 @@ pq = new TreeSet<>((a, b) -> { 除此之外,还要改变 `distance` 函数,**不能简单地让它计算一个线段两个端点间的长度,而是让它计算该线段中点和端点之间的长度**。 + ```java -private int distance(int[] intv) { - int x = intv[0]; - int y = intv[1]; - if (x == -1) return y; - if (y == N) return N - 1 - x; - // 中点和端点之间的长度 - return (y - x) / 2; +class ExamRoom { + // ... + + private int distance(int[] intv) { + int x = intv[0]; + int y = intv[1]; + if (x == -1) return y; + if (y == N) return N - 1 - x; + // 中点和端点之间的长度 + return (y - x) / 2; + } } ``` -![](../pictures/座位调度/5.jpg) +![](https://labuladong.online/algo/images/座位调度/5.jpg) 这样,`[0,4]` 和 `[4,9]` 的 `distance` 值就相等了,算法会比较二者的索引,取较小的线段进行分割。到这里,这道算法题目算是完全解决了。 @@ -199,17 +235,91 @@ private int distance(int[] intv) { 处理动态问题一般都会用到有序数据结构,比如平衡二叉搜索树和二叉堆,二者的时间复杂度差不多,但前者支持的操作更多。 -既然平衡二叉搜索树这么好用,还用二叉堆干嘛呢?因为二叉堆底层就是数组,实现简单啊,详见旧文「二叉堆详解」。你实现个红黑树试试?操作复杂,而且消耗的空间相对来说会多一些。具体问题,还是要选择恰当的数据结构来解决。 +既然平衡二叉搜索树这么好用,还用二叉堆干嘛呢?因为二叉堆底层就是数组,实现简单啊,详见前文 [二叉堆详解](https://labuladong.online/algo/data-structure-basic/binary-heap-implement/)。你实现个红黑树试试?操作复杂,而且消耗的空间相对来说会多一些。具体问题,还是要选择恰当的数据结构来解决。 希望本文对大家有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) -[上一篇:如何在无限序列中随机抽取元素](../高频面试系列/水塘抽样.md) -[下一篇:Union-Find算法详解](../算法思维系列/UnionFind算法详解.md) +**_____________** + +**《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「**全家桶**」可下载配套 PDF 和刷题全家桶**: + +![](https://labuladong.online/algo/images/souyisou2.png) + +======其他语言代码====== + +[855.考场就座](https://leetcode-cn.com/problems/exam-room) + +### javascript + +js内置没有treeset相关的实现,而且实现起来也比较麻烦。 + +array记录有人的位置 + +seat的情况如下: + +- 如果没有array时,seatNo默认0 +- 有array有一个时,则看离两边的距离,选距离远的 +- 两两遍历取中间值和初始值之和 + +```js +class ExamRoom { + /** + * @param {number} N + */ + constructor(N) { + this.array = []; + this.seatNo = 0; + this.number = N - 1; + } + /** + * @return {number} + */ + seat() { + this.seatNo = 0; + if (this.array.length == 1) { + if (this.array[0] == 0) { + this.seatNo = this.number; + } else if (this.array[0] == this.number) { + this.seatNo = 0; + } else { + let distance1 = this.array[0]; + let distance2 = this.number - this.array[0]; + if (distance1 >= distance2) { + this.seatNo = 0 + distance1; + } else { + this.seatNo = distance1 + distance2; + } + } + } else if ((this.array.length > 1)) { + let maxDistance = this.array[0], start; + for (let i = 0; i < this.array.length - 1; i++) { + let distance = Math.floor((this.array[i + 1] - this.array[i] >>> 1)); + if (maxDistance < distance) { + maxDistance = distance; + start = this.array[i] + this.seatNo = start + maxDistance; + } + } + if (this.number - this.array[this.array.length - 1] > maxDistance) { + this.seatNo = this.number; + } + } + this.array.push(this.seatNo); + this.array.sort((a, b) => { return a - b }) + return this.seatNo; + } + /** + * @param {number} p + * @return {void} + */ + leave(p) { + let index = this.array.indexOf(p) + this.array.splice(index, 1) + }; +} +``` -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" index 1ded2a791d..c344424166 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" @@ -1,8 +1,28 @@ # 如何高效寻找素数 + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [204. Count Primes](https://leetcode.com/problems/count-primes/) | [204. 计数质数](https://leetcode.cn/problems/count-primes/) | 🟠 | + +**-----------** + + + 素数的定义看起来很简单,如果一个数如果只能被 1 和它本身整除,那么这个数就是素数。 -不要觉得素数的定义简单,恐怕没多少人真的能把素数相关的算法写得高效。比如让你写这样一个函数: +虽然素数的定义并不复杂,恐怕没多少人真的能把素数相关的算法写得高效。 + +比如力扣第 204 题「计数质数」,让你写这样一个函数: ```java // 返回区间 [2, n) 中有几个素数 @@ -18,7 +38,7 @@ int countPrimes(int n) int countPrimes(int n) { int count = 0; for (int i = 2; i < n; i++) - if (isPrim(i)) count++; + if (isPrime(i)) count++; return count; } @@ -32,9 +52,9 @@ boolean isPrime(int n) { } ``` -这样写的话时间复杂度 O(n^2),问题很大。**首先你用 isPrime 函数来辅助的思路就不够高效;而且就算你要用 isPrime 函数,这样写算法也是存在计算冗余的**。 +这样写的话时间复杂度 O(n^2),问题很大。**首先你用 `isPrime` 函数来辅助的思路就不够高效;而且就算你要用 `isPrime` 函数,这样写算法也是存在计算冗余的**。 -先来简单说下**如果你要判断一个数是不是素数,应该如何写算法**。只需稍微修改一下上面的 isPrim 代码中的 for 循环条件: +先来说下**如果你要判断一个数是不是素数,应该如何写算法**。只需稍微修改一下上面的 `isPrime` 代码中的 for 循环条件: ```java boolean isPrime(int n) { @@ -45,6 +65,10 @@ boolean isPrime(int n) { 换句话说,`i` 不需要遍历到 `n`,而只需要到 `sqrt(n)` 即可。为什么呢,我们举个例子,假设 `n = 12`。 + + + + ```java 12 = 2 × 6 12 = 3 × 4 @@ -53,50 +77,63 @@ boolean isPrime(int n) { 12 = 6 × 2 ``` + + 可以看到,后两个乘积就是前面两个反过来,反转临界点就在 `sqrt(n)`。 换句话说,如果在 `[2,sqrt(n)]` 这个区间之内没有发现可整除因子,就可以直接断定 `n` 是素数了,因为在区间 `[sqrt(n),n]` 也一定不会发现可整除因子。 -现在,`isPrime` 函数的时间复杂度降为 O(sqrt(N)),**但是我们实现 `countPrimes` 函数其实并不需要这个函数**,以上只是希望读者明白 `sqrt(n)` 的含义,因为等会还会用到。 +现在,`isPrime` 函数的时间复杂度降为 $O(sqrt(N))$,**但是我们实现 `countPrimes` 函数其实并不需要这个函数**,以上只是希望读者明白 `sqrt(n)` 的含义,因为等会还会用到。 +## 高效实现 `countPrimes` -### 高效实现 `countPrimes` +接下来介绍的方法叫做「素数筛选法」,这个方法是古希腊一位名叫埃拉托色尼的大佬发明的,我们在中学的教课书上见过他的大名,因为他就是第一个通过物体的影子正确计算地球周长的人,被推崇为「地理学之父」。 -高效解决这个问题的核心思路是和上面的常规思路反着来: +回到正题,素数筛选法的核心思路是和上面的常规思路反着来: 首先从 2 开始,我们知道 2 是一个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8... 都不可能是素数了。 然后我们发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12... 也都不可能是素数了。 +Wikipedia 的这个 GIF 很形象: + +![](https://labuladong.online/algo/images/prime/1.gif) + 看到这里,你是否有点明白这个排除法的逻辑了呢?先看我们的第一版代码: ```java -int countPrimes(int n) { - boolean[] isPrim = new boolean[n]; - // 将数组都初始化为 true - Arrays.fill(isPrim, true); - - for (int i = 2; i < n; i++) - if (isPrim[i]) - // i 的倍数不可能是素数了 - for (int j = 2 * i; j < n; j += i) - isPrim[j] = false; - - int count = 0; - for (int i = 2; i < n; i++) - if (isPrim[i]) count++; - - return count; +class Solution { + public int countPrimes(int n) { + boolean[] isPrime = new boolean[n]; + // 将数组都初始化为 true + Arrays.fill(isPrime, true); + + for (int i = 2; i < n; i++) { + if (isPrime[i]) { + // i 的倍数不可能是素数了 + for (int j = 2 * i; j < n; j += i) { + isPrime[j] = false; + } + } + } + + int count = 0; + for (int i = 2; i < n; i++) { + if (isPrime[i]) count++; + } + + return count; + } } ``` 如果上面这段代码你能够理解,那么你已经掌握了整体思路,但是还有两个细微的地方可以优化。 -首先,回想刚才判断一个数是否是素数的 `isPrime` 函数,由于因子的对称性,其中的 for 循环只需要遍历 `[2,sqrt(n)]` 就够了。这里也是类似的,我们外层的 for 循环也只需要遍历到 `sqrt(n)`: +首先,回想本文开头介绍的 `isPrime` 素数判定函数,由于因子的对称性,其中的 for 循环只需要遍历 `[2,sqrt(n)]` 就够了。这里也是类似的,我们外层的 for 循环也只需要遍历到 `sqrt(n)`: ```java for (int i = 2; i * i < n; i++) - if (isPrim[i]) + if (isPrime[i]) ... ``` @@ -104,54 +141,103 @@ for (int i = 2; i * i < n; i++) ```java for (int j = 2 * i; j < n; j += i) - isPrim[j] = false; + isPrime[j] = false; ``` 这样可以把 `i` 的整数倍都标记为 `false`,但是仍然存在计算冗余。 -比如 `n = 25`,`i = 4` 时算法会标记 4 × 2 = 8,4 × 3 = 12 等等数字,但是这两个数字已经被 `i = 2` 和 `i = 3` 的 2 × 4 和 3 × 4 标记了。 +比如 `n = 25`,`i = 5` 时算法会标记 5 × 2 = 10,5 × 3 = 15 等等数字,但是这两个数字已经被 `i = 2` 和 `i = 3` 的 2 × 5 和 3 × 5 标记了。 -我们可以稍微优化一下,让 `j` 从 `i` 的平方开始遍历,而不是从 `2 * i` 开始: +我们可以稍微优化一下,让 `j` 从 `i * i` 开始遍历,而不是从 `2 * i` 开始: ```java for (int j = i * i; j < n; j += i) - isPrim[j] = false; + isPrime[j] = false; ``` 这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫做 Sieve of Eratosthenes。看下完整的最终代码: ```java -int countPrimes(int n) { - boolean[] isPrim = new boolean[n]; - Arrays.fill(isPrim, true); - for (int i = 2; i * i < n; i++) - if (isPrim[i]) - for (int j = i * i; j < n; j += i) - isPrim[j] = false; - - int count = 0; - for (int i = 2; i < n; i++) - if (isPrim[i]) count++; - - return count; +class Solution { + public int countPrimes(int n) { + boolean[] isPrime = new boolean[n]; + Arrays.fill(isPrime, true); + for (int i = 2; i * i < n; i++) { + if (isPrime[i]) { + for (int j = i * i; j < n; j += i) { + isPrime[j] = false; + } + } + } + + int count = 0; + for (int i = 2; i < n; i++) { + if (isPrime[i]) count++; + } + + return count; + } } ``` + +
+ +
+ +🌈 代码可视化动画🌈 + +
+
+
+ + + **该算法的时间复杂度比较难算**,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是: n/2 + n/3 + n/5 + n/7 + ... = n × (1/2 + 1/3 + 1/5 + 1/7...) -括号中是素数的倒数。其最终结果是 O(N * loglogN),有兴趣的读者可以查一下该算法的时间复杂度证明。 +括号中是素数的倒数。其最终结果是 $O(N * loglogN)$,有兴趣的读者可以查一下该算法的时间复杂度证明。 以上就是素数算法相关的全部内容。怎么样,是不是看似简单的问题却有不少细节可以打磨呀? -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:如何实现LRU算法](../高频面试系列/LRU算法.md) -[下一篇:如何计算编辑距离](../动态规划系列/编辑距离.md) -[目录](../README.md#目录) \ No newline at end of file + + +
+
+引用本文的文章 + + - [【强化练习】链表双指针经典习题](https://labuladong.online/algo/problem-set/linkedlist-two-pointers/) + - [一文秒杀所有丑数系列问题](https://labuladong.online/algo/frequency-interview/ugly-number-summary/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [264. Ugly Number II](https://leetcode.com/problems/ugly-number-ii/?show=1) | [264. 丑数 II](https://leetcode.cn/problems/ugly-number-ii/?show=1) | 🟠 | +| - | [剑指 Offer 49. 丑数](https://leetcode.cn/problems/chou-shu-lcof/?show=1) | 🟠 | + +
+
+ + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" index 6718ddb8ac..8b2173feea 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" @@ -1,10 +1,35 @@ -# 接雨水问题详解 +# 如何高效解决接雨水问题 -接雨水这道题目挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。 + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [11. Container With Most Water](https://leetcode.com/problems/container-with-most-water/) | [11. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) | 🟠 | +| [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/) | [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) | 🔴 | + +**-----------** + + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [数组双指针技巧汇总](https://labuladong.online/algo/essential-technique/array-two-pointers-summary/) + +力扣第 42 题「接雨水」挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。 先看一下题目: -![](../pictures/接雨水/title.png) + 就是用一个数组表示一个条形图,问你这个条形图最多能接多少水。 @@ -14,106 +39,135 @@ int trap(int[] height); 下面就来由浅入深介绍暴力解法 -> 备忘录解法 -> 双指针解法,在 O(N) 时间 O(1) 空间内解决这个问题。 -### 一、核心思路 +## 一、核心思路 -我第一次看到这个问题,无计可施,完全没有思路,相信很多朋友跟我一样。所以对于这种问题,我们不要想整体,而应该去想局部;就像之前的文章处理字符串问题,不要考虑如何处理整个字符串,而是去思考应该如何处理每一个字符。 +> [!TIP] +> 做算法题,如果对题目提出的问题没有思路,不妨尝试化简问题,先从局部思考,先写出最简单粗暴的解法,也许会有突破点。逐步优化后也许就能找到最优解。 -这么一想,可以发现这道题的思路其实很简单。具体来说,仅仅对于位置 i,能装下多少水呢? +比如这道题,先不考虑整个柱状图能装多少水,仅仅考虑位置 `i` 这一个位置能装下多少水? + +![](https://labuladong.online/algo/images/rain-water/0.jpg) + +能装 2 格水,因为 `height[i]` 的高度为 0,而这里最多能盛 2 格水,2-0=2。 + +为什么位置 `i` 最多能盛 2 格水呢?因为,位置 `i` 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 `l_max` 和 `r_max`;**位置 `i` 最大的水柱高度就是 `min(l_max, r_max)`**。 + +也就是说,对于位置 `i`,能够装的水为: -![](../pictures/接雨水/0.jpg) -能装 2 格水。为什么恰好是两格水呢?因为 height[i] 的高度为 0,而这里最多能盛 2 格水,2-0=2。 -为什么位置 i 最多能盛 2 格水呢?因为,位置 i 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 `l_max` 和 `r_max`;**位置 i 最大的水柱高度就是 `min(l_max, r_max)`。** -更进一步,对于位置 i,能够装的水为: ```python water[i] = min( - # 左边最高的柱子 - max(height[0..i]), - # 右边最高的柱子 - max(height[i..end]) - ) - height[i] - + # 左边最高的柱子 + max(height[0..i]), + # 右边最高的柱子 + max(height[i..end]) +) - height[i] ``` -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/1.jpg) +![](https://labuladong.online/algo/images/rain-water/1.jpg) + +![](https://labuladong.online/algo/images/rain-water/2.jpg) + -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/2.jpg) 这就是本问题的核心思路,我们可以简单写一个暴力算法: -```cpp -int trap(vector& height) { - int n = height.size(); - int ans = 0; - for (int i = 1; i < n - 1; i++) { - int l_max = 0, r_max = 0; - // 找右边最高的柱子 - for (int j = i; j < n; j++) - r_max = max(r_max, height[j]); - // 找左边最高的柱子 - for (int j = i; j >= 0; j--) - l_max = max(l_max, height[j]); - // 如果自己就是最高的话, - // l_max == r_max == height[i] - ans += min(l_max, r_max) - height[i]; +```java +class Solution { + public int trap(int[] height) { + int n = height.length; + int res = 0; + for (int i = 1; i < n - 1; i++) { + int l_max = 0, r_max = 0; + // 找右边最高的柱子 + for (int j = i; j < n; j++) + r_max = Math.max(r_max, height[j]); + // 找左边最高的柱子 + for (int j = i; j >= 0; j--) + l_max = Math.max(l_max, height[j]); + // 如果自己就是最高的话, + // l_max == r_max == height[i] + res += Math.min(l_max, r_max) - height[i]; + } + return res; } - return ans; } ``` -有之前的思路,这个解法应该是很直接粗暴的,时间复杂度 O(N^2),空间复杂度 O(1)。但是很明显这种计算 `r_max` 和 `l_max` 的方式非常笨拙,一般的优化方法就是备忘录。 - -### 二、备忘录优化 - -之前的暴力解法,不是在每个位置 i 都要计算 `r_max` 和 `l_max` 吗?我们直接把结果都缓存下来,别傻不拉几的每次都遍历,这时间复杂度不就降下来了嘛。 - -我们开两个**数组** `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 i 左边最高的柱子高度,`r_max[i]` 表示位置 i 右边最高的柱子高度。预先把这两个数组计算好,避免重复计算: - -```cpp -int trap(vector& height) { - if (height.empty()) return 0; - int n = height.size(); - int ans = 0; - // 数组充当备忘录 - vector l_max(n), r_max(n); - // 初始化 base case - l_max[0] = height[0]; - r_max[n - 1] = height[n - 1]; - // 从左向右计算 l_max - for (int i = 1; i < n; i++) - l_max[i] = max(height[i], l_max[i - 1]); - // 从右向左计算 r_max - for (int i = n - 2; i >= 0; i--) - r_max[i] = max(height[i], r_max[i + 1]); - // 计算答案 - for (int i = 1; i < n - 1; i++) - ans += min(l_max[i], r_max[i]) - height[i]; - return ans; +这个解法应该是很直接粗暴的,时间复杂度 O(N^2),空间复杂度 O(1)。但是很明显这种计算 `r_max` 和 `l_max` 的方式非常笨拙,每次都要用 for 循环去遍历,我们是不是可以优化一下这个过程? + +## 二、备忘录优化 + +之前的暴力解法,不是在每个位置 `i` 都要计算 `r_max` 和 `l_max` 吗?我们直接把结果都提前计算出来,别傻不拉几的每次都遍历,这时间复杂度不就降下来了嘛。 + +**我们开两个数组 `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 `i` 左边最高的柱子高度,`r_max[i]` 表示位置 `i` 右边最高的柱子高度**。预先把这两个数组计算好,避免重复计算: + +```java +class Solution { + public int trap(int[] height) { + if (height.length == 0) { + return 0; + } + int n = height.length; + int res = 0; + // 数组充当备忘录 + int[] l_max = new int[n]; + int[] r_max = new int[n]; + // 初始化 base case + l_max[0] = height[0]; + r_max[n - 1] = height[n - 1]; + // 从左向右计算 l_max + for (int i = 1; i < n; i++) + l_max[i] = Math.max(height[i], l_max[i - 1]); + // 从右向左计算 r_max + for (int i = n - 2; i >= 0; i--) + r_max[i] = Math.max(height[i], r_max[i + 1]); + // 计算答案 + for (int i = 1; i < n - 1; i++) + res += Math.min(l_max[i], r_max[i]) - height[i]; + return res; + } } ``` -这个优化其实和暴力解法差不多,就是避免了重复计算,把时间复杂度降低为 O(N),已经是最优了,但是空间复杂度是 O(N)。下面来看一个精妙一些的解法,能够把空间复杂度降低到 O(1)。 -### 三、双指针解法 +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +这个优化其实和暴力解法思路差不多,就是避免了重复计算,把时间复杂度降低为 O(N),已经是最优了,但是空间复杂度是 O(N)。下面来看一个精妙一些的解法,能够把空间复杂度降低到 O(1)。 + +## 三、双指针解法 + +> [!NOTE] +> 这个解法作为思路拓展,看看就好,不必过于执着最优解。因为对于大部分人,在真实的面试/笔试中,能够使用朴实无华的方法见招拆招,写出上面的解法就可以了。虽然多了一些空间复杂度,但一般判题平台还是能过的。 +> +> 除非过不了所有测试用例,且你写完了其他题目还有富余的时间,再花时间针对上面的解法进行优化也不迟。 这种解法的思路是完全相同的,但在实现手法上非常巧妙,我们这次也不要用备忘录提前计算了,而是用双指针**边走边算**,节省下空间复杂度。 首先,看一部分代码: -```cpp -int trap(vector& height) { - int n = height.size(); - int left = 0, right = n - 1; - - int l_max = height[0]; - int r_max = height[n - 1]; +```java +int trap(int[] height) { + int left = 0, right = height.length - 1; + int l_max = 0, r_max = 0; - while (left <= right) { - l_max = max(l_max, height[left]); - r_max = max(r_max, height[right]); + while (left < right) { + l_max = Math.max(l_max, height[left]); + r_max = Math.max(r_max, height[right]); + // 此时 l_max 和 r_max 分别表示什么? left++; right--; } } @@ -125,67 +179,168 @@ int trap(vector& height) { 明白了这一点,直接看解法: -```cpp -int trap(vector& height) { - if (height.empty()) return 0; - int n = height.size(); - int left = 0, right = n - 1; - int ans = 0; - - int l_max = height[0]; - int r_max = height[n - 1]; - - while (left <= right) { - l_max = max(l_max, height[left]); - r_max = max(r_max, height[right]); - - // ans += min(l_max, r_max) - height[i] - if (l_max < r_max) { - ans += l_max - height[left]; - left++; - } else { - ans += r_max - height[right]; - right--; +```java +class Solution { + public int trap(int[] height) { + int left = 0, right = height.length - 1; + int l_max = 0, r_max = 0; + + int res = 0; + while (left < right) { + l_max = Math.max(l_max, height[left]); + r_max = Math.max(r_max, height[right]); + + // res += min(l_max, r_max) - height[i] + if (l_max < r_max) { + res += l_max - height[left]; + left++; + } else { + res += r_max - height[right]; + right--; + } } + return res; } - return ans; } ``` 你看,其中的核心思想和之前一模一样,换汤不换药。但是细心的读者可能会发现次解法还是有点细节差异: -之前的备忘录解法,`l_max[i]` 和 `r_max[i]` 代表的是 `height[0..i]` 和 `height[i..end]` 的最高柱子高度。 +之前的备忘录解法,`l_max[i]` 和 `r_max[i]` 分别代表 `height[0..i]` 和 `height[i..end]` 的最高柱子高度。 -```cpp -ans += min(l_max[i], r_max[i]) - height[i]; +```java +res += Math.min(l_max[i], r_max[i]) - height[i]; ``` -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/3.jpg) +![](https://labuladong.online/algo/images/rain-water/3.jpg) 但是双指针解法中,`l_max` 和 `r_max` 代表的是 `height[0..left]` 和 `height[right..end]` 的最高柱子高度。比如这段代码: -```cpp +```java if (l_max < r_max) { - ans += l_max - height[left]; + res += l_max - height[left]; left++; } ``` -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/4.jpg) +![](https://labuladong.online/algo/images/rain-water/4.jpg) 此时的 `l_max` 是 `left` 指针左边的最高柱子,但是 `r_max` 并不一定是 `left` 指针右边最高的柱子,这真的可以得到正确答案吗? -其实这个问题要这么思考,我们只在乎 `min(l_max, r_max)`。对于上图的情况,我们已经知道 `l_max < r_max` 了,至于这个 `r_max` 是不是右边最大的,不重要,重要的是 `height[i]` 能够装的水只和 `l_max` 有关。 +其实这个问题要这么思考,我们只在乎 `min(l_max, r_max)`。**对于上图的情况,我们已经知道 `l_max < r_max` 了,至于这个 `r_max` 是不是右边最大的,不重要。重要的是 `height[i]` 能够装的水只和较低的 `l_max` 之差有关**: + +![](https://labuladong.online/algo/images/rain-water/5.jpg) + +这样,接雨水问题就解决了。 + +## 扩展:盛最多水的容器 + +下面我们看一道和接雨水问题非常类似的题目,力扣第 11 题「盛最多水的容器」: + + + + + + + +```java +// 函数签名如下 +int maxArea(int[] height); +``` + +这题和接雨水问题很类似,可以完全套用前文的思路,而且还更简单。两道题的区别在于: + +**接雨水问题给出的类似一幅直方图,每个横坐标都有宽度,而本题给出的每个横坐标是一条竖线,没有宽度**。 + +我们前文讨论了半天 `l_max` 和 `r_max`,实际上都是为了计算 `height[i]` 能够装多少水;而本题中 `height[i]` 没有了宽度,那自然就好办多了。 + +举个例子,如果在接雨水问题中,你知道了 `height[left]` 和 `height[right]` 的高度,你能算出 `left` 和 `right` 之间能够盛下多少水吗? + +不能,因为你不知道 `left` 和 `right` 之间每个柱子具体能盛多少水,你得通过每个柱子的 `l_max` 和 `r_max` 来计算才行。 + +反过来,就本题而言,你知道了 `height[left]` 和 `height[right]` 的高度,能算出 `left` 和 `right` 之间能够盛下多少水吗? + +可以,因为本题中竖线没有宽度,所以 `left` 和 `right` 之间能够盛的水就是: + +```python +min(height[left], height[right]) * (right - left) +``` + +类似接雨水问题,高度是由 `height[left]` 和 `height[right]` 较小的值决定的。 + +解决这道题的思路依然是双指针技巧: + +**用 `left` 和 `right` 两个指针从两端向中心收缩,一边收缩一边计算 `[left, right]` 之间的矩形面积,取最大的面积值即是答案**。 + +先直接看解法代码吧: + +```java +class Solution { + public int maxArea(int[] height) { + int left = 0, right = height.length - 1; + int res = 0; + while (left < right) { + // [left, right] 之间的矩形面积 + int cur_area = Math.min(height[left], height[right]) * (right - left); + res = Math.max(res, cur_area); + // 双指针技巧,移动较低的一边 + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + return res; + } +} +``` + + +
+ +
+ +🎃 代码可视化动画🎃 + +
+
+
+ + + +代码和接雨水问题大致相同,不过肯定有读者会问,下面这段 if 语句为什么要移动较低的一边: + + + + + +```java +// 双指针技巧,移动较低的一边 +if (height[left] < height[right]) { + left++; +} else { + right--; +} +``` + + + +**其实也好理解,因为矩形的高度是由 `min(height[left], height[right])` 即较低的一边决定的**: + +你如果移动较低的那一边,那条边可能会变高,使得矩形的高度变大,进而就「有可能」使得矩形的面积变大;相反,如果你去移动较高的那一边,矩形的高度是无论如何都不会变大的,所以不可能使矩形的面积变得更大。 + +至此,这道题也解决了。 + + + + + -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/5.jpg) -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:如何运用二分查找算法](../高频面试系列/koko偷香蕉.md) -[下一篇:如何去除有序数组的重复元素](../高频面试系列/如何去除有序数组的重复元素.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" deleted file mode 100644 index b4ea0c5d8e..0000000000 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" +++ /dev/null @@ -1,110 +0,0 @@ -# 如何寻找最长回文子串 - -回文串是面试常常遇到的问题(虽然问题本身没啥意义),本文就告诉你回文串问题的核心思想是什么。 - -首先,明确一下什:**回文串就是正着读和反着读都一样的字符串**。 - -比如说字符串 `aba` 和 `abba` 都是回文串,因为它们对称,反过来还是和本身一样。反之,字符串 `abac` 就不是回文串。 - -可以看到回文串的的长度可能是奇数,也可能是偶数,这就添加了回文串问题的难度,解决该类问题的核心是**双指针**。下面就通过一道最长回文子串的问题来具体理解一下回文串问题: - -![](../pictures/回文/title.png) - -```cpp -string longestPalindrome(string s) {} -``` - -### 一、思考 - -对于这个问题,我们首先应该思考的是,给一个字符串 `s`,如何在 `s` 中找到一个回文子串? - -有一个很有趣的思路:既然回文串是一个正着反着读都一样的字符串,那么如果我们把 `s` 反转,称为 `s'`,然后在 `s` 和 `s'` 中寻找**最长公共子串**,这样应该就能找到最长回文子串。 - -比如说字符串 `abacd`,反过来是 `dcaba`,它的最长公共子串是 `aba`,也就是最长回文子串。 - -但是这个思路是错误的,比如说字符串 `aacxycaa`,反转之后是 `aacyxcaa`,最长公共子串是 `aac`,但是最长回文子串应该是 `aa`。 - -虽然这个思路不正确,但是**这种把问题转化为其他形式的思考方式是非常值得提倡的**。 - -下面,就来说一下正确的思路,如何使用双指针。 - -**寻找回文串的问题核心思想是:从中间开始向两边扩散来判断回文串**。对于最长回文子串,就是这个意思: - -```python -for 0 <= i < len(s): - 找到以 s[i] 为中心的回文串 - 更新答案 -``` - -但是呢,我们刚才也说了,回文串的长度可能是奇数也可能是偶数,如果是 `abba`这种情况,没有一个中心字符,上面的算法就没辙了。所以我们可以修改一下: - -```python -for 0 <= i < len(s): - 找到以 s[i] 为中心的回文串 - 找到以 s[i] 和 s[i+1] 为中心的回文串 - 更新答案 -``` - -PS:读者可能发现这里的索引会越界,等会会处理。 - -### 二、代码实现 - -按照上面的思路,先要实现一个函数来寻找最长回文串,这个函数是有点技巧的: - -```cpp -string palindrome(string& s, int l, int r) { - // 防止索引越界 - while (l >= 0 && r < s.size() - && s[l] == s[r]) { - // 向两边展开 - l--; r++; - } - // 返回以 s[l] 和 s[r] 为中心的最长回文串 - return s.substr(l + 1, r - l - 1); -} -``` - -为什么要传入两个指针 `l` 和 `r` 呢?**因为这样实现可以同时处理回文串长度为奇数和偶数的情况**: - -```python -for 0 <= i < len(s): - # 找到以 s[i] 为中心的回文串 - palindrome(s, i, i) - # 找到以 s[i] 和 s[i+1] 为中心的回文串 - palindrome(s, i, i + 1) - 更新答案 -``` - -下面看下 `longestPalindrome` 的完整代码: - -```cpp -string longestPalindrome(string s) { - string res; - for (int i = 0; i < s.size(); i++) { - // 以 s[i] 为中心的最长回文子串 - string s1 = palindrome(s, i, i); - // 以 s[i] 和 s[i+1] 为中心的最长回文子串 - string s2 = palindrome(s, i, i + 1); - // res = longest(res, s1, s2) - res = res.size() > s1.size() ? res : s1; - res = res.size() > s2.size() ? res : s2; - } - return res; -} -``` - -至此,这道最长回文子串的问题就解决了,时间复杂度 O(N^2),空间复杂度 O(1)。 - -值得一提的是,这个问题可以用动态规划方法解决,时间复杂度一样,但是空间复杂度至少要 O(N^2) 来存储 DP table。这道题是少有的动态规划非最优解法的问题。 - -另外,这个问题还有一个巧妙的解法,时间复杂度只需要 O(N),不过该解法比较复杂,我个人认为没必要掌握。该算法的名字叫 Manacher's Algorithm(马拉车算法),有兴趣的读者可以自行搜索一下。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) - -[上一篇:如何去除有序数组的重复元素](../高频面试系列/如何去除有序数组的重复元素.md) - -[下一篇:如何k个一组反转链表](../高频面试系列/k个一组反转链表.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" deleted file mode 100644 index 0897e48f59..0000000000 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" +++ /dev/null @@ -1,122 +0,0 @@ -我最近在 LeetCode 上做到两道非常有意思的题目,382 和 398 题,关于水塘抽样算法(Reservoir Sampling),本质上是一种随机概率算法,解法应该说会者不难,难者不会。 - -我第一次见到这个算法问题是谷歌的一道算法题:给你一个**未知长度**的链表,请你设计一个算法,**只能遍历一次**,随机地返回链表中的一个节点。 - -这里说的随机是均匀随机(uniform random),也就是说,如果有 `n` 个元素,每个元素被选中的概率都是 `1/n`,不可以有统计意义上的偏差。 - -一般的想法就是,我先遍历一遍链表,得到链表的总长度 `n`,再生成一个 `[1,n]` 之间的随机数为索引,然后找到索引对应的节点,不就是一个随机的节点了吗? - -但题目说了,只能遍历一次,意味着这种思路不可行。题目还可以再泛化,给一个未知长度的序列,如何在其中随机地选择 `k` 个元素?想要解决这个问题,就需要著名的水塘抽样算法了。 - -### 算法实现 - -**先解决只抽取一个元素的问题**,这个问题的难点在于,随机选择是「动态」的,比如说你现在你有 5 个元素,你已经随机选取了其中的某个元素 `a` 作为结果,但是现在再给你一个新元素 `b`,你应该留着 `a` 还是将 `b` 作为结果呢,以什么逻辑选择 `a` 和 `b` 呢,怎么证明你的选择方法在概率上是公平的呢? - -**先说结论,当你遇到第 `i` 个元素时,应该有 `1/i` 的概率选择该元素,`1 - 1/i` 的概率保持原有的选择**。看代码容易理解这个思路: - -```java -/* 返回链表中一个随机节点的值 */ -int getRandom(ListNode head) { - Random r = new Random(); - int i = 0, res = 0; - ListNode p = head; - // while 循环遍历链表 - while (p != null) { - // 生成一个 [0, i) 之间的整数 - // 这个整数等于 0 的概率就是 1/i - if (r.nextInt(++i) == 0) { - res = p.val; - } - p = p.next; - } - return res; -} -``` - -对于概率算法,代码往往都是很浅显的,但是这种问题的关键在于证明,你的算法为什么是对的?为什么每次以 `1/i` 的概率更新结果就可以保证结果是平均随机(uniform random)? - -**证明**:假设总共有 `n` 个元素,我们要的随机性无非就是每个元素被选择的概率都是 `1/n` 对吧,那么对于第 `i` 个元素,它被选择的概率就是: - -$$ -\begin{aligned} - &\frac{1}{i} \times (1 - \frac{1}{i+1}) \times (1 - \frac{1}{i+2}) \times ... \times (1 - \frac{1}{n}) \\ - = &\frac{1}{i} \times \frac{i}{i+1} \times \frac{i+1}{i+2} \times ... \times \frac{n-1}{n} \\ - = &\frac{1}{n} -\end{aligned} -$$ - -第 `i` 个元素被选择的概率是 `1/i`,第 `i+1` 次不被替换的概率是 `1 - 1/(i+1)`,以此类推,相乘就是第 `i` 个元素最终被选中的概率,就是 `1/n`。 - -因此,该算法的逻辑是正确的。 - -**同理,如果要随机选择 `k` 个数,只要在第 `i` 个元素处以 `k/i` 的概率选择该元素,以 `1 - k/i` 的概率保持原有选择即可**。代码如下: - -```java -/* 返回链表中 k 个随机节点的值 */ -int[] getRandom(ListNode head, int k) { - Random r = new Random(); - int[] res = new int[k]; - ListNode p = head; - - // 前 k 个元素先默认选上 - for (int j = 0; j < k && p != null; j++) { - res[j] = p.val; - p = p.next; - } - - int i = k; - // while 循环遍历链表 - while (p != null) { - // 生成一个 [0, i) 之间的整数 - int j = r.nextInt(++i); - // 这个整数小于 k 的概率就是 k/i - if (j < k) { - res[j] = p.val; - } - p = p.next; - } - return res; -} -``` - -对于数学证明,和上面区别不大: - -$$ -\begin{aligned} - &\frac{k}{i} \times (1 - \frac{k}{i+1} \times \frac{1}{k}) \times (1 - \frac{k}{i+2} \times \frac{1}{k}) \times ... \times (1 - \frac{k}{n} \times \frac{1}{k}) \\ - = &\frac{k}{i} \times (1 - \frac{1}{i+1}) \times (1 - \frac{1}{i+2}) \times ... \times (1 - \frac{1}{n}) \\ - = &\frac{k}{i} \times \frac{i}{i+1} \times \frac{i+1}{i+2} \times ... \times \frac{n-1}{n} \\ - = &\frac{k}{n} -\end{aligned} -$$ - -因为虽然每次更新选择的概率增大了 `k` 倍,但是选到具体第 `i` 个元素的概率还是要乘 `1/k`,也就回到了上一个推导。 - -### 拓展延伸 - -以上的抽样算法时间复杂度是 O(n),但不是最优的方法,更优化的算法基于几何分布(geometric distribution),时间复杂度为 O(k + klog(n/k))。由于涉及的数学知识比较多,这里就不列出了,有兴趣的读者可以自行搜索一下。 - -还有一种思路是基于「Fisher–Yates 洗牌算法」的。随机抽取 `k` 个元素,等价于对所有元素洗牌,然后选取前 `k` 个。只不过,洗牌算法需要对元素的随机访问,所以只能对数组这类支持随机存储的数据结构有效。 - -另外有一种思路也比较有启发意义:给每一个元素关联一个随机数,然后把每个元素插入一个容量为 `k` 的二叉堆(优先级队列)按照配对的随机数进行排序,最后剩下的 `k` 个元素也是随机的。 - -这个方案看起来似乎有点多此一举,因为插入二叉堆需要 O(logk) 的时间复杂度,所以整个抽样算法就需要 O(nlogk) 的复杂度,还不如我们最开始的算法。但是,这种思路可以指导我们解决**加权随机抽样算法**,权重越高,被随机选中的概率相应增大,这种情况在现实生活中是很常见的,比如你不往游戏里充钱,就永远抽不到皮肤。 - -最后,我想说随机算法虽然不多,但其实很有技巧的,读者不妨思考两个常见且看起来很简单的问题: - -1、如何对带有权重的样本进行加权随机抽取?比如给你一个数组 `w`,每个元素 `w[i]` 代表权重,请你写一个算法,按照权重随机抽取索引。比如 `w = [1,99]`,算法抽到索引 0 的概率是 1%,抽到索引 1 的概率是 99%。 - -2、实现一个生成器类,构造函数传入一个很长的数组,请你实现 `randomGet` 方法,每次调用随机返回数组中的一个元素,多次调用不能重复返回相同索引的元素。要求不能对该数组进行任何形式的修改,且操作的时间复杂度是 O(1)。 - -这两个问题都是比较困难的,以后有时间我会写一写相关的文章。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:如何判断回文链表](../高频面试系列/判断回文链表.md) - -[下一篇:如何调度考生的座位](../高频面试系列/座位调度.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" deleted file mode 100644 index 01bdd63856..0000000000 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" +++ /dev/null @@ -1,114 +0,0 @@ -# 如何寻找消失的元素 - -之前也有文章写过几个有趣的智力题,今天再聊一道巧妙的题目。 - -题目非常简单: - -![](../pictures/缺失元素/title.png) - -给一个长度为 n 的数组,其索引应该在 `[0,n)`,但是现在你要装进去 n + 1 个元素 `[0,n]`,那么肯定有一个元素装不下嘛,请你找出这个缺失的元素。 - -这道题不难的,我们应该很容易想到,把这个数组排个序,然后遍历一遍,不就很容易找到缺失的那个元素了吗? - -或者说,借助数据结构的特性,用一个 HashSet 把数组里出现的数字都储存下来,再遍历 `[0,n]` 之间的数字,去 HashSet 中查询,也可以很容易查出那个缺失的元素。 - -排序解法的时间复杂度是 O(NlogN),HashSet 的解法时间复杂度是 O(N),但是还需要 O(N) 的空间复杂度存储 HashSet。 - -**第三种方法是位运算**。 - -对于异或运算(`^`),我们知道它有一个特殊性质:一个数和它本身做异或运算结果为 0,一个数和 0 做异或运算还是它本身。 - -而且异或运算满足交换律和结合律,也就是说: - -2 ^ 3 ^ 2 = 3 ^ (2 ^ 2) = 3 ^ 0 = 3 - -而这道题索就可以通过这些性质巧妙算出缺失的那个元素。比如说 `nums = [0,3,1,4]`: - -![](../pictures/缺失元素/1.jpg) - - -为了容易理解,我们假设先把索引补一位,然后让每个元素和自己相等的索引相对应: - -![](../pictures/缺失元素/2.jpg) - - -这样做了之后,就可以发现除了缺失元素之外,所有的索引和元素都组成一对儿了,现在如果把这个落单的索引 2 找出来,也就找到了缺失的那个元素。 - -如何找这个落单的数字呢,**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**,也就达到了我们的目的。 - -```java -int missingNumber(int[] nums) { - int n = nums.length; - int res = 0; - // 先和新补的索引异或一下 - res ^= n; - // 和其他的元素、索引做异或 - for (int i = 0; i < n; i++) - res ^= i ^ nums[i]; - return res; -} -``` - -![](../pictures/缺失元素/3.jpg) - -由于异或运算满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素的。 - -至此,时间复杂度 O(N),空间复杂度 O(1),已经达到了最优,我们是否就应该打道回府了呢? - -如果这样想,说明我们受算法的毒害太深,随着我们学习的知识越来越多,反而容易陷入思维定式,这个问题其实还有一个特别简单的解法:**等差数列求和公式**。 - -题目的意思可以这样理解:现在有个等差数列 0, 1, 2,..., n,其中少了某一个数字,请你把它找出来。那这个数字不就是 `sum(0,1,..n) - sum(nums)` 嘛? - -```java -int missingNumber(int[] nums) { - int n = nums.length; - // 公式:(首项 + 末项) * 项数 / 2 - int expect = (0 + n) * (n + 1) / 2; - - int sum = 0; - for (int x : nums) - sum += x; - return expect - sum; -``` - -你看,这种解法应该是最简单的,但说实话,我自己也没想到这个解法,而且我去问了几个大佬,他们也没想到这个最简单的思路。相反,如果去问一个初中生,他也许很快就能想到。 - -做到这一步了,我们是否就应该打道回府了呢? - -如果这样想,说明我们对细节的把控还差点火候。在用求和公式计算 `expect` 时,你考虑过**整型溢出**吗?如果相乘的结果太大导致溢出,那么结果肯定是错误的。 - -刚才我们的思路是把两个和都加出来然后相减,为了避免溢出,干脆一边求和一边减算了。很类似刚才位运算解法的思路,仍然假设 `nums = [0,3,1,4]`,先补一位索引再让元素跟索引配对: - -![](../pictures/缺失元素/xor.png) - - -我们让每个索引减去其对应的元素,再把相减的结果加起来,不就是那个缺失的元素吗? - -```java -public int missingNumber(int[] nums) { - int n = nums.length; - int res = 0; - // 新补的索引 - res += n - 0; - // 剩下索引和元素的差加起来 - for (int i = 0; i < n; i++) - res += i - nums[i]; - return res; -} -``` - -由于加减法满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素的。 - -至此这道算法题目经历九曲十八弯,终于再也没有什么坑了。 - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:如何判定括号合法性](../高频面试系列/合法括号判定.md) - -[下一篇:如何寻找缺失和重复的元素](../高频面试系列/缺失和重复的元素.md) - -[目录](../README.md#目录) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" index d541e839a2..92e5b0f7c9 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" @@ -1,12 +1,32 @@ -今天就聊一道很看起来简单却十分巧妙的问题,寻找缺失和重复的元素。之前的一篇文章「寻找缺失元素」也写过类似的问题,不过这次的和上次的问题使用的技巧不同。 +# 如何同时寻找缺失和重复的元素 -这是 LeetCode 645 题,我来描述一下这个题目: + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [645. Set Mismatch](https://leetcode.com/problems/set-mismatch/) | [645. 错误的集合](https://leetcode.cn/problems/set-mismatch/) | 🟢 | + +**-----------** + + + +今天就聊一道很看起来简单却十分巧妙的问题,寻找缺失和重复的元素。之前的一篇文章 [常用的位操作](https://labuladong.online/algo/frequency-interview/bitwise-operation/) 中也写过类似的问题,不过这次的和上次的问题使用的技巧不同。 + +这是力扣第 645 题「错误的集合」,我来描述一下这个题目: 给一个长度为 `N` 的数组 `nums`,其中本来装着 `[1..N]` 这 `N` 个元素,无序。但是现在出现了一些错误,`nums` 中的一个元素出现了重复,也就同时导致了另一个元素的缺失。请你写一个算法,找到 `nums` 中的重复元素和缺失元素的值。 -```cpp +```java // 返回两个数字,分别是 {dup, missing} -vector findErrorNums(vector& nums); +int[] findErrorNums(int[] nums); ``` 比如说输入:`nums = [1,2,2,4]`,算法返回 `[2,3]`。 @@ -15,9 +35,15 @@ vector findErrorNums(vector& nums); 但问题是,这个常规解法需要一个哈希表,也就是 O(N) 的空间复杂度。你看题目给的条件那么巧,在 `[1..N]` 的几个数字中恰好有一个重复,一个缺失,**事出反常必有妖**,对吧。 -O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想办法如何降低空间复杂度,是否可以在 O(1) 的空间复杂度之下找到重复和确实的元素呢? +O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想办法如何降低空间复杂度,是否可以在 O(1) 的空间复杂度之下找到重复和缺失的元素呢? -### 思路分析 + + + + + + +## 思路分析 这个问题的特点是,每个元素和数组索引有一定的对应关系。 @@ -31,29 +57,29 @@ O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想 那么,如何不使用额外空间判断某个索引有多少个元素对应呢?这就是这个问题的精妙之处了: -**通过将每个索引对应的元素变成负数,以表示这个索引被对应过一次了**: +**通过将每个索引对应的元素变成负数,以表示这个索引被对应过一次了**,算法过程如下 GIF 所示: -![](../pictures/dupmissing/1.gif) +![](https://labuladong.online/algo/images/dupmissing/1.gif) 如果出现重复元素 `4`,直观结果就是,索引 `4` 所对应的元素已经是负数了: -![](../pictures/dupmissing/2.jpg) +![](https://labuladong.online/algo/images/dupmissing/2.jpg) 对于缺失元素 `3`,直观结果就是,索引 `3` 所对应的元素是正数: -![](../pictures/dupmissing/3.jpg) +![](https://labuladong.online/algo/images/dupmissing/3.jpg) 对于这个现象,我们就可以翻译成代码了: -```cpp -vector findErrorNums(vector& nums) { - int n = nums.size(); +```java +int[] findErrorNums(int[] nums) { + int n = nums.length; int dup = -1; for (int i = 0; i < n; i++) { - int index = abs(nums[i]); + int index = Math.abs(nums[i]); // nums[index] 小于 0 则说明重复访问 if (nums[index] < 0) - dup = abs(nums[i]); + dup = Math.abs(nums[i]); else nums[index] *= -1; } @@ -64,38 +90,53 @@ vector findErrorNums(vector& nums) { if (nums[i] > 0) missing = i; - return {dup, missing}; + return new int[]{dup, missing}; } ``` 这个问题就基本解决了,别忘了我们刚才为了方便分析,假设元素是 `[0..N-1]`,但题目要求是 `[1..N]`,所以只要简单修改两处地方即可得到原题的答案: -```cpp -vector findErrorNums(vector& nums) { - int n = nums.size(); - int dup = -1; - for (int i = 0; i < n; i++) { - // 现在的元素是从 1 开始的 - int index = abs(nums[i]) - 1; - if (nums[index] < 0) - dup = abs(nums[i]); - else - nums[index] *= -1; +```java +class Solution { + public int[] findErrorNums(int[] nums) { + int n = nums.length; + int dup = -1; + for (int i = 0; i < n; i++) { + // 现在的元素是从 1 开始的 + int index = Math.abs(nums[i]) - 1; + if (nums[index] < 0) + dup = Math.abs(nums[i]); + else + nums[index] *= -1; + } + + int missing = -1; + for (int i = 0; i < n; i++) + if (nums[i] > 0) + // 将索引转换成元素 + missing = i + 1; + + return new int[]{dup, missing}; } - - int missing = -1; - for (int i = 0; i < n; i++) - if (nums[i] > 0) - // 将索引转换成元素 - missing = i + 1; - - return {dup, missing}; } ``` + +
+ +
+ +🥳 代码可视化动画🥳 + +
+
+
+ + + 其实,元素从 1 开始是有道理的,也必须从一个非零数开始。因为如果元素从 0 开始,那么 0 的相反数还是自己,所以如果数字 0 出现了重复或者缺失,算法就无法判断 0 是否被访问过。我们之前的假设只是为了简化题目,更通俗易懂。 -### 最后总结 +## 最后总结 对于这种数组问题,**关键点在于元素和索引是成对儿出现的,常用的方法是排序、异或、映射**。 @@ -103,17 +144,43 @@ vector findErrorNums(vector& nums) { 排序的方法也很好理解,对于这个问题,可以想象如果元素都被从小到大排序,如果发现索引对应的元素如果不相符,就可以找到重复和缺失的元素。 -异或运算也是常用的,因为异或性质 `a ^ a = 0, a ^ 0 = a`,如果将索引和元素同时异或,就可以消除成对儿的索引和元素,留下的就是重复或者缺失的元素。可以看看前文「寻找缺失元素」,介绍过这种方法。 +异或运算也是常用的,因为异或性质 `a ^ a = 0, a ^ 0 = a`,如果将索引和元素同时异或,就可以消除成对儿的索引和元素,留下的就是重复或者缺失的元素。可以看看前文 [常用的位运算](https://labuladong.online/algo/frequency-interview/bitwise-operation/),介绍过这种方法。 + + + + + + + +
+
+引用本文的文章 + + - [【强化练习】哈希表更多习题](https://labuladong.online/algo/problem-set/hash-table/) + +

+ + + + +
+
+引用本文的题目 + +安装 [我的 Chrome 刷题插件](https://labuladong.online/algo/intro/chrome/) 点开下列题目可直接查看解题思路: +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [442. Find All Duplicates in an Array](https://leetcode.com/problems/find-all-duplicates-in-an-array/?show=1) | [442. 数组中重复的数据](https://leetcode.cn/problems/find-all-duplicates-in-an-array/?show=1) | 🟠 | +| [448. Find All Numbers Disappeared in an Array](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/?show=1) | [448. 找到所有数组中消失的数字](https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/?show=1) | 🟢 | +
+
-坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:如何寻找消失的元素](../高频面试系列/消失的元素.md) -[下一篇:如何判断回文链表](../高频面试系列/判断回文链表.md) -[目录](../README.md#目录) \ No newline at end of file +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" new file mode 100644 index 0000000000..9144fd373b --- /dev/null +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" @@ -0,0 +1,259 @@ +# 带权重的随机选择算法 + + + +![](https://labuladong.online/algo/images/souyisou1.png) + +**通知:为满足广大读者的需求,网站上架 [速成目录](https://labuladong.online/algo/intro/quick-learning-plan/),如有需要可以看下,谢谢大家的支持~另外,建议你在我的 [网站](https://labuladong.online/algo/) 学习文章,体验更好。** + + + +读完本文,你不仅学会了算法套路,还可以顺便解决如下题目: + +| LeetCode | 力扣 | 难度 | +| :----: | :----: | :----: | +| [528. Random Pick with Weight](https://leetcode.com/problems/random-pick-with-weight/) | [528. 按权重随机选择](https://leetcode.cn/problems/random-pick-with-weight/) | 🟠 | +| - | [剑指 Offer II 071. 按权重生成随机数](https://leetcode.cn/problems/cuyjEf/) | 🟠 | + +**-----------** + + + +> [!NOTE] +> 阅读本文前,你需要先学习: +> +> - [前缀和算法技巧](https://labuladong.online/algo/data-structure/prefix-sum/) +> - [二分查找框架详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) + +写这篇的文章的原因是玩 LOL 手游。我有个朋友抱怨说打排位匹配的队友太菜了,我就说我打排位觉得队友都挺行的啊,好像不怎么坑? + +朋友意味深长地说了句:一般隐藏分比较高的玩家,排位如果排不到实力相当的队友,就会排到一些菜狗。 + +嗯?我想了几秒钟感觉这小伙子不对劲,他意思是说我隐藏分低,还是说我就是那条菜狗? + +我立马要求和他开黑打一把,证明我不是菜狗,他才是菜狗。开黑结果这里不便透露,大家猜猜吧。 + +打完之后我就来发文了,因为我对游戏的匹配机制有了一点思考。 + +![](https://labuladong.online/algo/images/random-weight/images.png) + +**所谓「隐藏分」我不知道是不是真的,毕竟匹配机制是所有竞技类游戏的核心环节,想必非常复杂,不是简单几个指标就能搞定的**。 + +但是如果把这个「隐藏分」机制简化,倒是一个值得思考的算法问题:系统如何以不同的随机概率进行匹配? + +或者简单点说,如何带权重地做随机选择? + +不要觉得这个很容易,如果给你一个长度为 `n` 的数组,让你从中等概率随机抽取一个元素,你肯定会做,random 一个 `[0, n-1]` 的数字出来作为索引就行了,每个元素被随机选到的概率都是 `1/n`。 + +但假设每个元素都有不同的权重,权重地大小代表随机选到这个元素的概率大小,你如何写算法去随机获取元素呢? + +力扣第 528 题「按权重随机选择」就是这样一个问题: + + + +我们就来思考一下这个问题,解决按照权重随机选择元素的问题。 + + + + + + + +## 解法思路 + +首先回顾一下我们和随机算法有关的历史文章: + +前文 [设计随机删除元素的数据结构](https://labuladong.online/algo/data-structure/random-set/) 主要考察的是数据结构的使用,每次把元素移到数组尾部再删除,可以避免数据搬移。 + +前文 [谈谈游戏中的随机算法](https://labuladong.online/algo/frequency-interview/random-algorithm/) 讲的是经典的「水塘抽样算法」,运用简单的数学运算,在无限序列中等概率选取元素。 + +前文 [算法笔试技巧](https://labuladong.online/algo/other-skills/tips-in-exam/) 中我还分享过一个巧用概率最大化测试用例通过率的骗分技巧。 + +**不过上述旧文并不能解决本文提出的问题,反而是前文 [前缀和技巧](https://labuladong.online/algo/data-structure/prefix-sum/) 加上 [二分搜索详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) 能够解决带权重的随机选择算法**。 + +这个随机算法和前缀和技巧和二分搜索技巧能扯上啥关系?且听我慢慢道来。 + +假设给你输入的权重数组是 `w = [1,3,2,1]`,我们想让概率符合权重,那么可以抽象一下,根据权重画出这么一条彩色的线段: + +![](https://labuladong.online/algo/images/random-weight/1.jpeg) + +如果我在线段上面随机丢一个石子,石子落在哪个颜色上,我就选择该颜色对应的权重索引,那么每个索引被选中的概率是不是就是和权重相关联了? + +**所以,你再仔细看看这条彩色的线段像什么?这不就是 [前缀和数组](https://labuladong.online/algo/data-structure/prefix-sum/) 嘛**: + +![](https://labuladong.online/algo/images/random-weight/2.jpeg) + + + + + + + +那么接下来,如何模拟在线段上扔石子? + +当然是随机数,比如上述前缀和数组 `preSum`,取值范围是 `[1, 7]`,那么我生成一个在这个区间的随机数 `target = 5`,就好像在这条线段中随机扔了一颗石子: + +![](https://labuladong.online/algo/images/random-weight/3.jpeg) + +还有个问题,`preSum` 中并没有 5 这个元素,我们应该选择比 5 大的最小元素,也就是 6,即 `preSum` 数组的索引 3: + +![](https://labuladong.online/algo/images/random-weight/4.jpeg) + +**如何快速寻找数组中大于等于目标值的最小元素?[二分搜索算法](https://labuladong.online/algo/essential-technique/binary-search-framework/) 就是我们想要的**。 + +到这里,这道题的核心思路就说完了,主要分几步: + +1、根据权重数组 `w` 生成前缀和数组 `preSum`。 + +2、生成一个取值在 `preSum` 之内的随机数,用二分搜索算法寻找大于等于这个随机数的最小元素索引。 + +3、最后对这个索引减一(因为前缀和数组有一位索引偏移),就可以作为权重数组的索引,即最终答案: + +![](https://labuladong.online/algo/images/random-weight/5.jpeg) + + + + + + + +## 解法代码 + +上述思路应该不难理解,但是写代码的时候坑可就多了。 + +要知道涉及开闭区间、索引偏移和二分搜索的题目,需要你对算法的细节把控非常精确,否则会出各种难以排查的 bug。 + +下面来抠细节,继续前面的例子: + +![](https://labuladong.online/algo/images/random-weight/3.jpeg) + +就比如这个 `preSum` 数组,你觉得随机数 `target` 应该在什么范围取值?闭区间 `[0, 7]` 还是左闭右开 `[0, 7)`? + +都不是,应该在闭区间 `[1, 7]` 中选择,**因为前缀和数组中 0 本质上是个占位符**,仔细体会一下: + +![](https://labuladong.online/algo/images/random-weight/6.jpeg) + +所以要这样写代码: + +```java +int n = preSum.length; +// target 取值范围是闭区间 [1, preSum[n - 1]] +int target = rand.nextInt(preSum[n - 1]) + 1; +``` + +接下来,在 `preSum` 中寻找大于等于 `target` 的最小元素索引,应该用什么品种的二分搜索?搜索左侧边界的还是搜索右侧边界的? + +实际上应该使用搜索左侧边界的二分搜索: + +```java +// 搜索左侧边界的二分搜索 +int left_bound(int[] nums, int target) { + if (nums.length == 0) return -1; + int left = 0, right = nums.length; + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left; +} +``` + +前文 [二分搜索详解](https://labuladong.online/algo/essential-technique/binary-search-framework/) 着重讲了数组中存在目标元素重复的情况,没仔细讲目标元素不存在的情况,这里补充一下。 + +**当目标元素 `target` 不存在数组 `nums` 中时,搜索左侧边界的二分搜索的返回值可以做以下几种解读**: + +1、返回的这个值是 `nums` 中大于等于 `target` 的最小元素索引。 + +2、返回的这个值是 `target` 应该插入在 `nums` 中的索引位置。 + +3、返回的这个值是 `nums` 中小于 `target` 的元素个数。 + +比如在有序数组 `nums = [2,3,5,7]` 中搜索 `target = 4`,搜索左边界的二分算法会返回 2,你带入上面的说法,都是对的。 + +所以以上三种解读都是等价的,可以根据具体题目场景灵活运用,显然这里我们需要的是第一种。 + +综上,我们可以写出最终解法代码: + +```java +class Solution { + // 前缀和数组 + private int[] preSum; + private Random rand = new Random(); + + public Solution(int[] w) { + int n = w.length; + // 构建前缀和数组,偏移一位留给 preSum[0] + preSum = new int[n + 1]; + preSum[0] = 0; + // preSum[i] = sum(w[0..i-1]) + for (int i = 1; i <= n; i++) { + preSum[i] = preSum[i - 1] + w[i - 1]; + } + } + + public int pickIndex() { + int n = preSum.length; + // Java 的 nextInt(n) 方法在 [0, n) 中生成一个随机整数 + // 再加一就是在闭区间 [1, preSum[n - 1]] 中随机选择一个数字 + int target = rand.nextInt(preSum[n - 1]) + 1; + // 获取 target 在前缀和数组 preSum 中的索引 + // 别忘了前缀和数组 preSum 和原始数组 w 有一位索引偏移 + return left_bound(preSum, target) - 1; + } + + // 搜索左侧边界的二分搜索 + private int left_bound(int[] nums, int target) { + int left = 0, right = nums.length; + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left; + } +} +``` + +有了之前的铺垫,相信你能够完全理解上述代码,这道随机权重的题目就解决了。 + +经常有读者留言调侃,每次都是看我的文章「云刷题」,看完就会了,也不用亲自动手刷了。 + +但我想说的是,很多题目思路一说就懂,但是深入一些的话很多细节都可能有坑,本文讲的这道题就是一个例子,所以还是建议多实践,多总结。 + + + + + + + + + +
+
+引用本文的文章 + + - [用数组加强哈希表(ArrayHashMap)](https://labuladong.online/algo/data-structure-basic/hashtable-with-array/) + - [谈谈游戏中的随机算法](https://labuladong.online/algo/frequency-interview/random-algorithm/) + +

+ + + + + +**_____________** + + + +![](https://labuladong.online/algo/images/souyisou2.png) \ No newline at end of file